Create a Slider Component with React and Framer Motion
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.