Create a Slider Component with React and Framer Motion

Learn how to create an awesome slider component with React and Framer Motion. It is actually really simple to create your own sliders from scratch.

•

Create a Slider Component with React and Framer Motion

Framer Motion React Slider

Introduction

Sliders have become an increasingly popular feature in modern web design. While there are many libraries available, they often fall short in terms of smoothness and customization.

This is where React and Framer Motion come into play, allowing you to build sliders that are not just robust and configurable, but also exceptionally smooth. In this tutorial, we’ll walk you through creating your own slider component using these powerful tools.

As a React developer I really enjoy the powerful combination of React and Framer Motion.

Setting Up the Slider Component

First, let’s import the necessary modules and set up the basic structure of our slider component. First of all you need to install framer-motion:

npm install framer-motion

import { AnimatePresence, motion } from "framer-motion";
import { ReactNode, useEffect, useState } from "react";
import { clsx } from "clsx";
import SliderNav from "@/components/ui/SliderNav";
import SliderPagination from "@/components/ui/SliderPagination";
import useSlider, { ResponsiveConfig, SliderConfig } from "@/hooks/useSlider";

Here, we are importing AnimatePresence and motion from Framer Motion, essential for animating our slider. We also import React hooks and other components that we'll use.

The Slider Component

Our slider component takes in a few props like configuration settings, responsive configurations, a render function for the items, and the data array.

import { AnimatePresence, motion } from "framer-motion";
import { ReactNode, useEffect, useState } from "react";
import { clsx } from "clsx";
import SliderNav from "@/components/ui/SliderNav";
import SliderPagination from "@/components/ui/SliderPagination";
import useSlider, { ResponsiveConfig, SliderConfig } from "@/hooks/useSlider";

interface Props {
  config?: SliderConfig;
  responsiveConfig?: ResponsiveConfig[];
  renderItem: (item: any, index: number) => ReactNode;
  data: any[];
}

const Slider = ({ config, responsiveConfig = [], renderItem, data }: Props) => {
  const {
    currentSlide,
    canGoNext,
    canGoPrev,
    scrollTo,
    settings,
    setCurrentSlide,
    nextSlide,
    prevSlide,
    setScrollTo,
    slideWidth,
  } = useSlider(data, config, responsiveConfig);

  return (
    <div className={"w-full overflow-x-visible"}>
      <motion.div
        initial={{ x: 0 }}
        animate={{
          x: `-${(100 / data.length) * currentSlide}%`,
        }}
        style={{
          width: `${slideWidth < 100 ? 100 : slideWidth}%`,
        }}
        transition={{
          type: "tween",
          duration: 0.5,
        }}
      >
        <motion.div
          className={"flex flex-row flex-nowrap"}
          style={{
            marginLeft: `-${settings.spacing}px`,
            marginRight: `-${settings.spacing}px`,
          }}
          onWheel={(e) => {
            if (e.deltaX < -1) {
              if (scrollTo) return;
              prevSlide();
              setScrollTo(true);
            } else if (e.deltaX > 1) {
              if (scrollTo) return;
              nextSlide();
              setScrollTo(true);
            } else {
              setScrollTo(false);
            }
          }}
          drag="x"
          dragConstraints={{ left: 0, right: 0 }}
          onDragEnd={(e, { offset, velocity }) => {
            const swipe = Math.abs(offset.x) > 50 && Math.abs(velocity.x) > 500;
            if (swipe && offset.x > 0) prevSlide();
            else if (swipe) nextSlide();
          }}
        >
          {data.map((item, index) => {
            return (
              <div
                key={index}
                style={{
                  width: `${100 / settings.slidesToShow}%`,
                  paddingLeft: `${settings.spacing}px`,
                  paddingRight: `${settings.spacing}px`,
                }}
              >
                {renderItem && renderItem(item, index)}
              </div>
            );
          })}
        </motion.div>
      </motion.div>
      <div className={"flex no-wrap items-center  space-x-4 mt-6"}>
        {settings.dots && (
          <SliderPagination
            slides={data.length - settings.slidesToShow + 1}
            onChange={setCurrentSlide}
            activeSlide={currentSlide}
          />
        )}
        {settings.arrows && (
          <div className={"shrink-0"}>
            <SliderNav
              canGoPrev={canGoPrev}
              canGoNext={canGoNext}
              onPrev={prevSlide}
              onNext={nextSlide}
            />
          </div>
        )}
      </div>
    </div>
  );
};

export default Slider;

In this snippet, we define the Slider function component. It utilizes a custom hook useSlider to manage the slider's state and settings. We use motion.div to animate the slide transition, ensuring a smooth user experience.

Slider Navigation Component

The SliderNav component provides navigation functionality for our slider.

import { clsx } from "clsx";

interface Props {
  onPrev: () => void;
  onNext: () => void;
  canGoPrev: boolean;
  canGoNext: boolean;
}

const SliderNav = ({ onPrev, onNext, canGoPrev, canGoNext }: Props) => {
  return (
    <div className={"space-x-2"}>
      <button
        onClick={onPrev}
        className={clsx(
          "transition-all duration-300",
          canGoPrev ? "opacity-100" : "opacity-20 cursor-not-allowed"
        )}
      >
        <svg
          width="37"
          height="36"
          viewBox="0 0 37 36"
          fill="none"
          className={"transform rotate-180"}
          xmlns="http://www.w3.org/2000/svg"
        >
          <rect
            x="1"
            y="0.5"
            width="35"
            height="35"
            rx="17.5"
            stroke="#222222"
          />
          <path d="M16.5 14L20.5 18L16.5 21.7461" stroke="#222222" />
        </svg>
      </button>
      <button
        onClick={onNext}
        className={clsx(
          "transition-all duration-300",
          canGoNext ? "opacity-100" : "opacity-20 cursor-not-allowed"
        )}
      >
        <svg
          width="37"
          height="36"
          viewBox="0 0 37 36"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <rect
            x="1"
            y="0.5"
            width="35"
            height="35"
            rx="17.5"
            stroke="#222222"
          />
          <path d="M16.5 14L20.5 18L16.5 21.7461" stroke="#222222" />
        </svg>
      </button>
    </div>
  );
};

export default SliderNav;

This component receives functions for moving to the next and previous slides and boolean values to determine if these actions are possible. It renders buttons for navigation, which are styled based on the slider’s state.

Note: clsx is used to combine classes.

Slider Pagination Component

The SliderPagination component is responsible for the slider's pagination functionality. Feel free to change the look of this.

import { motion } from "framer-motion";

interface Props {
  slides: number;
  activeSlide: number;
  onChange: (index: number) => void;
}

const SliderPagination = ({ slides, activeSlide, onChange }: Props) => {
  return (
    <div className={"h-[1px] w-full bg-black/20 relative overflow-hidden"}>
      <motion.div
        animate={{
          width: `${(100 / slides) * (activeSlide + 1)}%`,
        }}
        transition={{
          duration: 0.5,
          type: "tween",
        }}
        className={`h-[1px] absolute left-0 w-full bg-black/100 transition duration-500`}
      />
    </div>
  );
};

export default SliderPagination;

The useSlider Hook

Lastly, let’s discuss the custom hook useSlider that powers our slider.

import { useState, useEffect } from "react";

export interface SliderConfig {
  slidesToShow?: number;
  slidesToScroll?: number;
  infinite?: boolean;
  dots?: boolean;
  arrows?: boolean;
  spacing?: number;
}

export interface ResponsiveConfig {
  breakpoint: number;
  settings: SliderConfig;
}

const useSlider = (
  data: any[],
  config: SliderConfig = {},
  responsiveConfig: ResponsiveConfig[] = []
) => {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [canGoNext, setCanGoNext] = useState(false);
  const [canGoPrev, setCanGoPrev] = useState(false);
  const [scrollTo, setScrollTo] = useState<boolean>(false);
  const [resized, setResized] = useState<boolean>(false);

  const [settings, setSettings] = useState({
    slidesToShow: 1,
    slidesToScroll: 1,
    infinite: false,
    dots: true,
    arrows: true,
    spacing: 0,
    responsive: [],
    ...config,
  });

  const nextSlide = () => {
    if (!canGoNext) return;
    setCurrentSlide((prev) => (prev + 1) % data.length);
  };

  const prevSlide = () => {
    if (!canGoPrev) return;
    setCurrentSlide((prev) => (prev - 1 + data.length) % data.length);
  };

  useEffect(() => {
    if (settings.infinite) {
      setCanGoNext(true);
      setCanGoPrev(true);
    } else {
      setCanGoNext(currentSlide < data.length - settings.slidesToShow);
      setCanGoPrev(currentSlide > 0);
    }
  }, [settings.infinite, settings.slidesToShow, currentSlide, data.length]);

  const slideWidth = (100 * data.length) / settings.slidesToShow;

  const handleResize = () => {
    const width = window.innerWidth;
    if (responsiveConfig.length === 0) return;

    const responsiveSettings = responsiveConfig.find(
      (item) => item.breakpoint < width
    );

    if (responsiveSettings) {
      setSettings({
        ...settings,
        ...responsiveSettings.settings,
      });
    }
  };

  useEffect(() => {
    if (!resized) {
      handleResize();
      setResized(true);
      return;
    }
  }, [resized]);

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  });

  return {
    currentSlide,
    canGoNext,
    canGoPrev,
    scrollTo,
    settings,
    slideWidth,
    setScrollTo,
    setCurrentSlide,
    nextSlide,
    prevSlide,
  };
};

export default useSlider;

This hook manages the slider’s state, like the current slide and whether we can go to the next or previous slides. It also handles responsive settings and window resize events to ensure our slider looks great on all devices.

Lets see it in action

It is very flexible and can be used in various ways. This is one example:

<Slider
  config={{
    slidesToShow: 1,
    spacing: 4,
  }}
  responsiveConfig={[
    {tyoe
      breakpoint: 1280,
      settings: {
        slidesToShow: 4,
      },
    },
    {
      breakpoint: 1024,
      settings: {
        slidesToShow: 3,
      },
    },
    {
      breakpoint: 640,
      settings: {
        slidesToShow: 2,
      },
    },
  ]}
  data={data}
  renderItem={(item, index) => {
  // return an item of your choosing
    return (
      <BlogPostCard
        title={item.title}
        image={item.image}
        video={item.video}
        link={item.link}
        key={index}
      />
    );
  }}
/>

Conclusion

Creating your own slider with React and Framer Motion gives you the power to customize and achieve that perfect smoothness often lacking in pre-built libraries. With this guide, you’re now equipped to implement your own smooth, responsive sliders that will enhance the user experience on your website.

Continue Learning

Discover more articles on similar topics