How to Create an Animated Carousel in React

How to Create an Animated Carousel in React

Using CSS transitions
Ferenc Almasi β€’ Last updated 2024 March 01 β€’ Read time 17 min read β€’ React v18.2
Learn how you can build an animated Carousel component in React with the help of CSS transitions.
  • twitter
  • facebook
React Previous Tutorial

Carousels are a common way to present a list of media in a limited space. In this tutorial, we'll take a look at how you can create a Carousel component in React that can receive a list of images and be animated using CSS transitions. By the end of this tutorial, we'll have the following carousel ready to be used:

animated carousel in React
The final version of the animated carousel in React

How to Call the Carousel

First, we're going to need to see how we want to call the carousel. It'll accept an images prop through which we can pass an array of images for display:

Copied to clipboard! Playground
import { Carousel } from './Carousel'

const App = () => {
    const images = [
        {
            src: '/path/to/img.png',
            alt: 'Alternate text for image',
            url: '/link-to-page',
            caption: 'Image Caption next to CTA'
        },
        { ... },
        { ... },
        { ... }
    ]

    return <Carousel images={images} />
}
App.jsx
Calling the Carousel component

Each image object within the images array can have the following four different properties:

  • src: The file path to the image.
  • alt: The alternative text for the image, used as the alt attribute.
  • url: The URL to which the user will be directed when clicking the carousel image.
  • caption: A caption to be displayed on the image.
πŸ” Login to get access to the full source code in one piece. With TypeScript included.

Now that we know how the carousel will work, let's take a look at the component itself. If you haven't done so already, create a new file named Carousel.jsx and include the following:

Copied to clipboard! Playground
export const Carousel = ({ images }) => {
    return (
        <div className="carousel">
            <ul>
                {images.map((image, index) => 
                    <li key={index}>
                        <a href={image.url}>
                            <img
                                src={image.src}
                                alt={image.alt}
                            />
                            <div className="meta">
                                <span className="cta">Learn More</span>
                                <span className="caption">{image.caption}</span>
                            </div>
                        </a>
                    </li>
                )}
            </ul>
        </div>
    )
}
Carousel.jsx
Create the base of the carousel

This code generates a ul with a list of images that are passed to the component. It's important to wrap the ul within a wrapper for proper carousel styling later. Inside each li element, we need to reuse the image properties in various places:

  • Line 7: Use image.url for the href attribute of the anchor.
  • Line 9-10: Apply image.src and image.alt for the src and alt attributes of the image.
  • Line 14: Use image.caption within the .caption element.

Here is where you can modify the CTA text if you'd like to use different wording.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

Now that we have the necessary DOM elements for the carousel, we need to style it accordingly so it actually looks and behaves like a carousel. For the carousel container, we'll need the following rules:

Copied to clipboard! Playground
.carousel {
    max-width: 2500px;
    width: 100%;
    overflow: hidden;
    padding: 150px 0;
}
styles.css
Your max-width may differ depending on your use-case

We need to include overflow: hidden to ensure that the width of the carousel doesn't exceed 100% of the screen.

Optionally, you can choose to add an ::after element to create a fade-out effect for the images that are farthest from the center. This effect can be achieved using a radial gradient:

Copied to clipboard! Playground
.carousel::after {
    content: '';
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    background: radial-gradient(circle, rgba(17,12,17,0) 30%, rgba(17,12,17,1) 90%);
}
styles.css
You can use cssgradient.io to generate gradients

If you opt for this effect, make sure you include pointer-events: none to allow the images to receive click events; otherwise, they'll be blocked by the overlaying element.

Moving forward, the list is displayed as a flex container, centered on the screen. Since we want to apply a sliding animation when navigating the carousel, we need to add a transition: transform:

Copied to clipboard! Playground
.carousel ul {
    margin: 0;
    padding: 0;
    display: flex;
    gap: 20px;
    list-style-type: none;
    justify-content: center;
    transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
}

.carousel ul.even {
    transform: translateX(-250px);
}

.carousel img {
    width: 500px;
    height: 250px;
    object-fit: cover; /* Make sure you set this to avoid streching */
    display: block;
    border-radius: 5px;
}
styles.css
Make sure you remove the list-style-type

This is also where we want to adjust the carousel for even number of images. To achieve this, we'll use a helper class called .even. Given that each image has a width of 500px, we'll need to apply half of that width for the adjustment.

Adjusting carousel position based on number of images
Image will always be centered on the viewport if we adjust the carousel by half of the width of an image

By using half the width of an image for adjustment, we can ensure that the active image remains centered on the screen, regardless of the number of images present. Next, we have the anchor that wraps the entire image along with its caption.

For this, we need to set its position to relative and its overflow to hidden, as we'll be positioning the CTA and the caption absolutely within this element.

Copied to clipboard! Playground
.carousel a {
    position: relative;
    overflow: hidden;
    display: block;
}
styles.css
Add positioning and overflow to anchor

This will ensure that whenever the CTA and the caption overflow the image, it'll not be visible. As we navigate through the carousel, we want the CTA and the caption to smoothly slide and fade in from the bottom. To achieve this, add the following code to your CSS file:

Copied to clipboard! Playground
.carousel .meta {
    position: absolute;
    display: flex;
    align-items: center;
    gap: 10px;
    bottom: 20px;
    left: 20px;
    font-weight: bold;
    transition: all .6s cubic-bezier(0.4, 0.0, 0.2, 1);
    transform: translateY(75px);
    opacity: 0;
}

.carousel ul li.active .meta {
    transform: translateY(0);
    opacity: 1;
}

.carousel .cta {
    background: #FFF;
    padding: 10px 15px;
    display: inline-block;
    color: #222;
    border-radius: 5px;
}

.carousel .caption {
    color: #FFF;
    text-shadow: 0 0 5px #000;
}
styles.css
Style the CTA and the caption

The important parts are highlighted. Initially, the .meta element is offset by 75px and has an opacity of 0, placing it outside of the visible image area. Because we have overflow: hidden set on the anchor, it'll not be visible.

The image in the center will always have an .active class, which resets both the transform and opacity properties to their initial values. This brings the CTA and the caption back into view.

CTA and caption animated
CTA and caption animated into view for active image

Adding State to the Carousel

Now that we have all the CSS details sorted out, let's dive back into coding the actual interactivity. We're going to need two useState hooks to keep track of the internal state of the carousel. Extend the Carousel component with the following lines of code:

Copied to clipboard! Playground
import { useState } from 'react'

export const Carousel = ({ images }) => {
    const even = images.length % 2 === 0

    const [activeIndex, setActiveIndex] = useState(Math.floor(images.length / 2))
    const [transform, setTransform] = useState(even ? -250 : 0)

    return (
        <div className="carousel">
            <ul
                className={even ? 'even' : undefined}
                style={{ transform: `translateX(${transform}px)` }}
            >
                {images.map((image, index) => 
                    <li
                        key={index}
                        className={index === activeIndex ? 'active' : undefined}
                    >
                        <a href={image.url}>
                            <img
                                src={image.src}
                                alt={image.alt}
                            />
                            <div className="meta">
                                <span className="cta">Learn More</span>
                                <span className="caption">{image.caption}</span>
                            </div>
                        </a>
                    </li>
                )}
            </ul>
            <div className="arrows">
                <span
                    className={activeIndex == 0 ? 'left disabled' : 'left'}
                />
                <span
                    className={activeIndex == images.length - 1 ? 'right disabled' : 'right'}
                />
            </div>
        </div>
    )
}
Carousel.jsx
Add hooks to the Carousel component

Let's go through the changes to see what has been added:

  • Line 4: We can use the remainder (modulo) operator on images.length to calculate if there's an even number of images. This is used for center-aligning the images, adjusting their alignment based on the number present in the carousel.
  • Line 6: We need to store the index of the currently active image to apply corresponding styles. The initial index will consistently be the midpoint of the array, which we can get using images.length / 2.
  • Line 7: We also need a state variable for tracking the transformation amount. If the number of images is even, we want this value to be adjusted to -250 (half the width of an image); otherwise, it'll be 0.
  • Lines 12-13: Both the even and transform variables can be used to apply specific CSS styles to our carousel.
  • Lines 33-40: Additionally, we need two navigation arrows that we can use for traversing the carousel. Based on the activeIndex state, we can apply a .disabled class to the icon, indicating that no more items are available in that particular direction.

Styling the Navigation

To style the navigation, we'll need to add some additional CSS to our styles file. For creating arrows in CSS, include the following rules:

Copied to clipboard! Playground
.carousel .arrows {
    display: flex;
    justify-content: center;
    gap: 50px;
    margin-top: 20px;
    user-select: none;
}

.carousel .arrows span {
    cursor: pointer;
    border: solid #FFF;
    border-width: 0 4px 4px 0;
    display: inline-block;
    padding: 4px;
}

.carousel .arrows span.disabled {
    border-color: #555;
    cursor: default;
}

.carousel .arrows .left {
    transform: rotate(135deg);
}

.carousel .arrows .right {
    transform: rotate(-45deg);
}
styles.css
Style the navigation

Here, you'll want to set user-select to none, and cursor to pointer to ensure that the arrows are unselectable while displaying a pointer cursor, indicating interactivity.

To create arrows in CSS, apply styling to only the bottom and right borders, and introduce a rotation effect. Take a look at how they appear both without and with transformations applied:

πŸ” Login to get access to the full source code in one piece. With TypeScript included.

Adding the Navigation Functionality

Now, there's one final step remaining: adding the navigation functionality. To achieve this, we need to introduce a new function. Extend the Carousel component with the following function before the return statement:

Copied to clipboard! Playground
const slide = direction => {
    if (direction === 'left' && activeIndex !== 0) {
        setActiveIndex(activeIndex - 1)
        setTransform(transform + 520)
    }

    if (direction === 'right' && activeIndex !== images.length - 1) {
        setActiveIndex(activeIndex + 1)
        setTransform(transform - 520)
    }
}
Carousel.jsx
Add the slide functionality

This function is responsible for sliding the carousel in both directions. The direction parameter can take values of either "left" or "right". Regardless of the direction, we need to update both states:

  • Increment or decrement the activeIndex count by one, based on the direction.
  • Update the transform of the carousel by +/-520 (accounting for a 20px gap between images).

Ensure you validate activeIndex against 0 and images.length to prevent navigation beyond the available number of images.

This function needs to be called when interacting with the arrows. Add the highlighted parts to Carousel.jsx:

Copied to clipboard! Playground
<div className="arrows">
    <span
        className={activeIndex == 0 ? 'left disabled' : 'left'}
        onClick={() => slide('left')}
    />
    <span
        className={activeIndex == images.length - 1 ? 'right disabled' : 'right'}
        onClick={() => slide('right')}
    />
</div>
Carousel.jsx
Call the slide function

In essence, our carousel is now fully functional. However, let's also improve the accessibility by enabling users to navigate by clicking on the surrounding images. Add another click event to the li that calls another function with the index of the image:

Copied to clipboard!
<li onClick={(event) => slideByImage(event, index)}>
Carousel.jsx
Add click event listener on li

Make sure you pass the event along, as it's required inside the function. The slideByImage function only needs to internally call the slide function with the appropriate value:

Copied to clipboard! Playground
const slideByImage = (event, index) => {
    if (index !== activeIndex) {
        event.preventDefault()
        slide(index < activeIndex ? 'left' : 'right')
    }
}
Carousel.jsx
Add the slideByImage function

If the index doesn't match activeIndex, it signifies that we've clicked on a neighboring image rather than the focused image. In this case, we need to include event.preventDefault to prevent triggering the default browser action (navigating to the anchor URL).

Based on whether the index is less or greater than activeIndex, we can determine whether to call the slide function with a "left" or "right" value.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

Summary

Congratulations! You've just completed the creation of your very first CSS-animated carousel in React! Interested in elevating this project even further? Here are some ideas to enhance the configurability of this carousel:

  • Introduce infinite scrolling
  • Allow navigation through swipes and keyboard interactions
  • Display dots instead of arrows for navigation

Are you looking for more React projects? Check out our React projects roadmap, a comprehensive guide that offers inspiration on what to build next and how to approach it. Thank you for reading through, happy coding! 🎠

React Projects Roadmap
  • twitter
  • facebook
React
Did you find this page helpful?
πŸ“š More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.