• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

From scratch: Build a CSS Animated React Carousel Component

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
14,205
Баллы
155
Welcome to From scratch, our series focused on creating complex UI components with React, Typescript, and CSS4/PostCSS. We'll walk through the process step-by-step, emphasizing browser-first approach to achieve optimal performance and a smaller code footprint by making the most of native browser APIs and features.
In this article, we'll build a CSS animated, React-powered Carousel component. Carousels are like the circus of UIs: they spin, dazzle, and keep users engaged with rotating content. Whether it’s a kid’s amusement park or a digital dashboard, rotation isn’t just motion, it’s magic.

Carousel architecture


This video demonstrates a model of our component. We’ll create a real (or at least, believable) image carousel that rotates smoothly, always keeping one slide fully visible.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



This is the HTML code we will use to create such a carousel.


<div class="carousel">
<div class="scene">
<div class="viewport">
<div class="cell-1">
<img
src="https://..."
width="666"
height="333"
/>
</div>
<div class="cell-2">
<img
src="https://..."
width="666"
height="333"
/>
</div>
<div class="cell-3">
<img
src="https://..."
width="666"
height="333"
/>
</div>
</div>
</div>
</div>
.carousel


Sets up the responsive container.


.carousel {
--width: 0;
--height: 0;

display: flex;
flex-direction: column;
max-width: 100%;
width: calc(var(--width) * 1px);
}

Here, we set two CSS variables: --width and --height (both in px) to set a visual constrains of the carousel. This way we can access their value either in all children's CSS selectors or externally via a React hook (more on this later). We also make the carousel use CSS Flexbox layout, arranging items vertically. Moreover, we use max-width: 100%; to apply responsiveness.

.scene


It is required to maintain a reference coordinates and a consistent aspect ratio for all slides.


.scene {
aspect-ratio: calc(var(--width) / var(--height));
overflow: visible;
position: relative;
}

aspect-ratio property is a key to maintaining consistent proportions across all slides. It calculates the aspect ratio (--width divided by --height) using CSS variables and applies it to the scene. This ensures that images or content within the carousel maintain their correct shape regardless of the container size. Additionally, we set position: relative;, which allows us to position elements absolutely within the scene (like images or navigation controls).

.viewport


This div will be used to apply 3D transforms required to create the perspective and space effects. Individual cells will be positioned and rotated to show different parts of the content.

.cell-x


We need these divs to position each slide in 3D space with a unique rotation angle, creating the illusion of depth and allowing for smooth transitions between slides when viewed from different perspectives.


.cell-x {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;

& img {
display: block;
}
}

We add absolute positioning and CSS Flexbox to center content within the cell, and to fit the cell precisely within its container using offsets from the top and left edges. & img selector removes default styling from images (like extra spacing) to ensure they fit perfectly within the cell.

Positioning cells



Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



This schema shows a carousel inscribed in a single unit circle (R = 1). We will use trigonometry to calculate the position for each image. Each position will be defined by α: cell rotation angle, and T: distance from between the center of the cell and x=0, y=0coordinates.

Adjusting rotation


This is where math meets design. Each slide rotates by α = 360° / total cells, creating a seamless loop. For example, with 3 slides, each spins 120°.


.cell-3 {
--index: 3;
--cell-rotate: calc(var(--index) * (360 / var(--cells-amount)));
transform: rotateY(calc(var(--cell-rotate) * 1deg));
}


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Apply translate


Now we have to find T size. From the trigonometry cycle scheme, we can see that the length of T is equal to the BC side of the right triangle ABC. And the opposite angle equals to α° / 2. This way, we can get the following formula using tangent:

To create depth, have to find T size. This ensures slides are spaced just right, like slices of a pie. From the trigonometry cycle scheme, we can see that the length of T is equal to the BC side of the right triangle ABC. And the opposite angle equals to α° / 2. This way, we can get the following formula using tangent:


T = W / 2 * 1 / tan(α° / 2)

Now let's shorten and remove α from the equation.


T = W / (2 * tan(π / cells amount))

.cell {
--t: calc(var(--width) / (2 * tan(pi / var(--cells-amount))));

transform: translateZ(calc(var(--t) * 1px));
}


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Now our DOM looks like the Museum of Modern Art in New York City. Which is artsy, but we want more Disneyland carousel vibes. We need to step out of the circle and center .viewport on the certain cell.

Here is the CSS code to achieve this.

--rotate calculates the initial rotation angle of the viewport based on which cell it should display.

--visibleIndex is the index of the visible cell from the start (0, 1, 2, …).

--translate calculates the initial translation distance along the Z-axis based on the viewport's width and the number of cells.

transform property rotates and moves the viewport, allowing the user to see the current cell from outside the hall. This achieved by negating values which were applied before the currently visible cell. Notice the -1 pixel multiplication factor. We also apply transition and ensure that 3D acceleration is enabled.


.viewport {
--rotate: calc(var(--visibleIndex) * (360 / var(--cells-amount)));
--translate: calc(var(--width) / (2 * tan(pi / var(--cells-amount))));
transform: translateZ(calc(var(--translate) * -1px))
rotateY(calc(var(--rotate) * -1deg));
transform-style: preserve-3d;
transition: transform 666ms ease-out;
}


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Making React component


Here is the UI we are going to build. Our goal is to give user ability to rotate the carousel. We will connect React state with CSS using two utility libraries:

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

to manage CSS classes and

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

to manage CSS variables.

We'll build Carousel React component at src/Carousel/Carousel.tsx.

Component interface

  • width/height: Define carousel dimensions;
  • defaultVisible: Which item shows initially (defaults to 0);
  • showDots/showArrows: Control visibility of navigation elements;
  • onRotate: Callback when user changes the active item;
  • exposedMode: Special view mode that shows the 3D structure.
Implement rotation hook


src/Carousel/useCarouselRotation.ts hook is responsible for managing the state of which cell is currently visible in a carousel and providing functions to navigate between these items.

We store the overall carousel moves amount as rotations. visibleCellIndex provides an index of currently focused cell based on number of rotations. For positive numbers, it uses a simple modulo: index % length. For negative, adds the length: length + (index % length). This creates a circular effect where rotating past the last cell loops back to the first and vice versa. rotateRight and rotateLeft are used to change rotation count by 1.


const [rotations, setRotations] = useState(defaultVisible);

const getVisibleCellIndex = (index: number, length: number) => {
return index % length >= 0 ? index % length : length + (index % length);

const rotateRight = useCallback(() => {
const nextRotation = rotations + 1;
setRotations(nextRotation);
onRotate(getVisibleCellIndex(nextRotation, cellsAmount));
}, [rotations, onRotate, cellsAmount]);
};
Use rotation state in Carousel


Now we have to consume the data provided by this hook. We will connect them with corresponding CSS variables with the help of the useLocalTheme hook, which creates a local scope of CSS variables and allows us to control them from the component.


import { useLocalTheme } from 'css-vars-hook'

const cellsAmount = Children.toArray(children).length;

const { visibleCellIndex, rotateRight, rotateLeft, rotations } =
useCarouselRotation({ defaultVisible, cellsAmount, onRotate });

const { LocalRoot, ref } = useLocalTheme<HTMLDivElement>();

const theme = useMemo(
() => ({
'aspect-ratio': width / height,
width: width,
'cells-amount': cellsAmount,
rotations: rotations,
}),
[width, height, cellsAmount, rotations],
);

return (
<LocalRoot theme={theme} className={classes.carousel}>
{/*...*/}
</LocalRoot>
)

Now we can access these values at div.carousel and its children CSS code.


.carousel {
width: calc(var(--width) * 1px);
}

.scene {
aspect-ratio: var(--aspect-ratio);
}

.viewport {
--rotate: calc(var(--rotations) * (360 / var(--cells-amount)));
--translate: calc(var(--width) / (2 * tan(pi / var(--cells-amount))));
}
Make carousel cells


The Carousel component will be able to accept any homogenous array of React nodes as children. All children are expected to have the same visual dimensions.


<Carousel width={666} height={333}>
<img
src={`

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

`}
width={666}
height={333}
/>
<img
src={`

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

`}
width={666}
height={333}
/>
</Carousel>

To apply the required transformations, we will wrap them with src/Carousel/Cell.tsx components.


import { Cell } from './Cell.tsx';

const cells = useMemo(
() =>
Children.toArray(children).map((element, index) => {
return (
<Cell index={index} key={index}>
{element}
</Cell>
);
}),
[children],
);

return (
<LocalRoot theme={theme} className={classes.carousel}>
{cells}
</LocalRoot>
)

Inside src/Carousel/Cell.tsx we will set up a new CSS variables scope for each cell.


import { useLocalTheme } from 'css-vars-hook';

export const Cell = ({
children,
index,
}) => {
const theme = useMemo(
() => ({
index,
}),
[index],
);
const { LocalRoot } = useLocalTheme();

return (
<LocalRoot theme={theme} className={classes.cell}>
{children}
</LocalRoot>
);
};

Inside CSS, we will use index value to calculate cell rotation angle.


.cell {
--cell-rotate: calc(var(--index) * (360 / var(--cells-amount)));

transform: rotateY(calc(var(--cell-rotate) * 1deg))
translateZ(calc(var(--translate) * 1px));
}
Add navigation and exposed mode


We will add navigation elements and exposed mode class based on the props.


import { Dots } from './Dots.tsx';

export const Carousel = ({
children,
showDots = true,
showArrows = true,
exposedMode
}) => {
// ...
return (
<LocalRoot theme={theme} className={classes.carousel}>
<div
className={classNames(classes.scene, {
[classes.normal]: !exposedMode,
[classes.exposed]: exposedMode,
})}
>
{showArrows && (
<button
className={classNames(classes.navigation, classes.left)}
onClick={rotateLeft}
>
<span className={classes.icon}>◄</span>
</button>
)}
<div className={classes.viewport}>{cells}</div>
{showArrows && (
<button
className={classNames(classes.navigation, classes.right)}
onClick={rotateRight}
>
<span className={classes.icon}>►</span>
</button>
)}
</div>
{showDots && (
<Dots
amount={Children.toArray(children).length}
active={visibleCellIndex}
/>
)}
</LocalRoot>
);
};
Make carousel responsive


Fixed width and height don't look right on mobile devices with small screens. To fix that, we will make Carousel responsive.


First step, we will add max-width: 100%; constrain to .carousel CSS class.

Next we have to measure the actual responsive with of Carousel and use this data to calculate sizes.

src/Carousel/useResponsiveWidth.ts dynamically tracks the actual width of a carousel container element, allowing it to maintain proper proportions and 3D positioning. useResizeObserver hook returns dimension information when the referenced element changes size. When a new width is detected, the effect updates the responsiveWidth state.


export const useResponsiveWidth = ({width, ref}: Props) => {
const [responsiveWidth, setResponsiveWidth] = useState(width);
const rect = useResizeObserver(ref);
useEffect(() => {
typeof rect?.width === 'number' && setResponsiveWidth(rect?.width);
}, [rect?.width]);
return responsiveWidth
}

Now we have to set --responsive-width alongside the initial --width. The former is used inside for container div and the latter for radius calculation.


const responsiveWidth = useResponsiveWidth({ width, ref });

const theme = useMemo(
() => ({
width: width,
'responsive-width': responsiveWidth,
}),
[width, height, responsiveWidth, cellsAmount, rotations],
);

.carousel {
width: calc(var(--width) * 1px);
}

.viewport {
--translate: calc(
var(--responsive-width) / (2 * tan(pi / var(--cells-amount)))
);
}
Demo


Here is a working demo of the carousel.


Happy coding.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу