Building a slider with animated images using React

In this tutorial we will build a React component slider with sliced images layout, with animation on mouse hover and on switching between slides. I will divide the tutorial into 3 section: creating slider, sliced image animation, and image animation on mouse hover.

A few weeks ago I came across post on LinkedIn about this event called Women Who Code: Days of Code Challenge. I feel like it's always fun to work on side project outside of work, and who knows, maybe I could make my Github "greener" than last year 😉. So I registered myself to this event and commited to a 7 days code challenge. I will share my reflection on the experience in another post. On this post, I will talk more about the project that I decided to work on these past 7 days.

Recently, I came across this website and it has this beautiful slider animation on its homepage: https://www.parquesdesintra.pt/en/. Since it looks so sleek and nice, I am curious on how they build the slider. I examined the code in the browser and found out their interesting solution using this CSS property called "object-position" for the mouse movement animation. It looks exciting, so I decided to recreate this slider with different approach for the 7 days code challenge. And the most exciting part is of course I will travel to the park next week (the main reason why I came accross of this site is to book their ticket 😄).

To give you an idea what we will build, I put the final result here: Demo preview. You can find the final result code on Github helloimela/sliced-slider.

This is a static image screenshot of the final result:

image

Choosing tech stack

To make things easier and to start faster, I used Stackblitz with React boilerplate and connect it to my Github account. I added a few libraries to code things better such as: Sass for styling with CSS modules and classnames to combine css module classes in React. For the images, I used this website: Lorem Picsum.

Creating slider

First of all, let's prepare the content. We have the slides added through a JSON data with this format:

// data.ts
{
  "id": 1,
  "url": "Some url",
  "title": "When ocean rises",
  "images": [
    {"src": "https://picsum.photos/id/12/800/600"},
    {"src": "https://picsum.photos/id/13/800/600"},
    // add as many images as needed per slide
  ]
}

Then, we create a neccessary state. The idea is to match the slide id with the current active id saved in the state. We create this in a component called Slider. This component is the main component that will handle the state for the slider. We will have children component to build slide item and slide navigation and they will be called SlideItem and SlideNavigation. The state will be passed to children component through React component props.

// src/components/Slider.tsx
const [activeSlide, setActiveSlide] = useState<number>();

Now, we need to create a component for each item in the slider. This component is called SlideItem. This component needs to hold all the images per slide. You can see the whole component code here. We passed the value of the images url, slide url, slide title and active state from Slider component to the SlideItem component.

// src/components/SlideItem.tsx

export const SlideItem: FC<SlideItemProps> = ({ images, url, title, active }) => {
return (
  <div className={cn(styles.slideItemContainer, active ? styles.activeSlide : '')}>
    <h2 className={styles.slideTitle}>{title}</h2>
    <ul className={styles.slideListParent}>
      {images.map(({ src }, index) => (
        <li
          key={index}
          className={styles.slideItemList}
        >
          <SlideImage src={src} isActive={active} className={className} />
        </li>
      ))}
    </ul>
    <a className={styles.slideButton} href={url}>Discover</a>
  </div>
  )
}

Each image on the slide item is handled in the SlideImage component. Images are added to the component as a background image.

And those are the steps for the first part. With using React state and children props, we have created a slider React component.

Animating images when switching between slides

Now we want to create a nice animation on each sliced-images. When switching betwen slides, every image slices are moving up and down on the Y axis, and sliding into the view on the X axis.

To be able to do this, we will utilize a change in CSS property transform: translate(). To add the animation effect, we added a transition property to the <li> element, which is the parent of SlideImage component. We will do all of this in the SlideItem component, because it should be applied differently to each slide item.

/* src/components/SlideItem.module.scss */

transition: transform 1s ease;

Let's talk about the animation on the Y axis. We will assign a random offset Y value everytime the slide changes.

First, we will create a function to calculate a random Y axis and save the value into a state.

// src/components/SlideItem.tsx

const [offsetY, setOffsetY] = useState<number[]>([]);
const calcOffsetY = (index: number): string => {
  // generate a random value between 1 and 30
  // this will move the image slices up and down between 1-30 pixels
  const rand = Math.floor(Math.random() * 30) + 1;
    if (index !== 0 && index%1) {
      return `${rand * -1}px`;
    } else {
      return `${rand * -1}px`;
    }
  }

And then, we call this function everytime the slider switches between slides. We can check the state by adding a listener to the active props using React useEffect.

// src/components/SlideItem.tsx

useEffect(() => {
    const offsets = [];
    images.map((image, index) => {
      const value = calcOffsetY(index);
      offsets.push(value);
    });
  setOffsetY(offsets);
}, [active]);

And then, we assign the offset Y value into our <li> element by adding the transform: translateY() to the style attribute.

image

For the animation on the X axis, we "hide" the image on the non-active slides by assigning a class based on their order. For example, if the current active slide is slide number 3, slide 1 and slide 2 should be moved to the left, and slide 4 should be moved to the right. With this, when we switch to slide 1, it will slide into the view from the left. And current slide will slide out of the view to the right.

So, we assign classes based on the slide position in the SlideImage component. I added 1px here because there's a slight pixel breaking out when we move inactive slide out of the view.

/* src/components/SlideImage.module.scss */

.slideImage {
  &.inactiveLeft {
    transform: translateX(calc(-100% - 1px));
  }

  &.inactiveRight {
    transform: translateX(calc(100% + 1px));
  }

  &.currentActive {
    transform: translateX(0);
  }
}

And then, we set the class on the parent Slider component using this function.


// src/components/Slider.tsx

const setActiveClass = (id: number, activeId: number): string => {
  if (id < activeId) {
      return "inactiveLeft";
    } else if (id > activeId) {
      return "inactiveRight";
    }
    return "currentActive";
  }

{slides.map(({ id, images, url, title }) => (
  <SlideItem
    key={id}
    id ={id}
    images={images}
    url={url}
    title={title}
    active={id === activeSlide ? true : false}
    className={setActiveClass(id, activeSlide)}
  />
))}

The code above will result in something like below.

On the video above, we can see the animation when we switched between slides.

Animating images on hover

As I mentioned above, in the original website, object-position was used to move the image on mouse hover. But I used a different approach and using a background image position X. On every mouse move, we detect the mouse X position relatives to the window's width. We attached the event to each slide so we add this in the SlideItem component. We passed this value to the SlideImage offset props.

// Slider.tsx
  const [hoverOffset, setHoverOffset] = useState<string>('50%');
  
  const handleSlideHover = (e: React.MouseEvent) => {
    const innerWidth = window.innerWidth;
    const calcOffset = (e.clientX / innerWidth) * 100;
    setHoverOffset(`${calcOffset}%`);
  };

  // ...
  <SlideImage src={src} offset={hoverOffset} isActive={active} className={className} />

To achieve this animation, there's an alternative to use CSS Variable declared in the parent component. With CSS Variable, we don't need to pass the value into children props. I compared both ways from a performance perspective on this post.

Summary

So here it is, we have created sleek sliced-images slider similar to this site, utilizing CSS properties such as background image position, transform translate, and CSS transition for a smooth animation.

Check out the final code on Github: Github repository. You can see the demo here: Demo preview.