Progressive Carousel
An animated progressive carousel component built with Framer Motion. Features smooth, dynamic transitions between items for a modern and engaging user experience.
An animated progressive carousel component built with Framer Motion. Features smooth, dynamic transitions between items for a modern and engaging user experience.
npm install motion
1import React, {2createContext,3useContext,4useState,5useEffect,6useRef,7ReactNode,8FC,9} from 'react';10import { motion, AnimatePresence } from 'motion/react';11import { cn } from '@/lib/utils';1213// Define the type for the context value14interface ProgressSliderContextType {15active: string;16progress: number;17handleButtonClick: (value: string) => void;18vertical: boolean;19}2021// Define the type for the component props22interface ProgressSliderProps {23children: ReactNode;24duration?: number;25fastDuration?: number;26vertical?: boolean;27activeSlider: string;28className?: string;29}3031interface SliderContentProps {32children: ReactNode;33className?: string;34}3536interface SliderWrapperProps {37children: ReactNode;38value: string;39className?: string;40}4142interface ProgressBarProps {43children: ReactNode;44className?: string;45}4647interface SliderBtnProps {48children: ReactNode;49value: string;50className?: string;51progressBarClass?: string;52}5354// Create the context with an undefined initial value55const ProgressSliderContext = createContext<56ProgressSliderContextType | undefined57>(undefined);5859export const useProgressSliderContext = (): ProgressSliderContextType => {60const context = useContext(ProgressSliderContext);61if (!context) {62throw new Error(63'useProgressSliderContext must be used within a ProgressSlider'64);65}66return context;67};6869export const ProgressSlider: FC<ProgressSliderProps> = ({70children,71duration = 5000,72fastDuration = 400,73vertical = false,74activeSlider,75className,76}) => {77const [active, setActive] = useState<string>(activeSlider);78const [progress, setProgress] = useState<number>(0);79const [isFastForward, setIsFastForward] = useState<boolean>(false);80const frame = useRef<number>(0);81const firstFrameTime = useRef<number>(performance.now());82const targetValue = useRef<string | null>(null);83const [sliderValues, setSliderValues] = useState<string[]>([]);8485useEffect(() => {86const getChildren = React.Children.toArray(children).find(87(child) => (child as React.ReactElement).type === SliderContent88) as React.ReactElement | undefined;8990if (getChildren) {91const values = React.Children.toArray(getChildren.props.children).map(92(child) => (child as React.ReactElement).props.value as string93);94setSliderValues(values);95}96}, [children]);9798useEffect(() => {99if (sliderValues.length > 0) {100firstFrameTime.current = performance.now();101frame.current = requestAnimationFrame(animate);102}103return () => {104cancelAnimationFrame(frame.current);105};106}, [sliderValues, active, isFastForward]);107108const animate = (now: number) => {109const currentDuration = isFastForward ? fastDuration : duration;110const elapsedTime = now - firstFrameTime.current;111const timeFraction = elapsedTime / currentDuration;112113if (timeFraction <= 1) {114setProgress(115isFastForward116? progress + (100 - progress) * timeFraction117: timeFraction * 100118);119frame.current = requestAnimationFrame(animate);120} else {121if (isFastForward) {122setIsFastForward(false);123if (targetValue.current !== null) {124setActive(targetValue.current);125targetValue.current = null;126}127} else {128// Move to the next slide129const currentIndex = sliderValues.indexOf(active);130const nextIndex = (currentIndex + 1) % sliderValues.length;131setActive(sliderValues[nextIndex]);132}133setProgress(0);134firstFrameTime.current = performance.now();135}136};137138const handleButtonClick = (value: string) => {139if (value !== active) {140const elapsedTime = performance.now() - firstFrameTime.current;141const currentProgress = (elapsedTime / duration) * 100;142setProgress(currentProgress);143targetValue.current = value;144setIsFastForward(true);145firstFrameTime.current = performance.now();146}147};148149return (150<ProgressSliderContext.Provider151value={{ active, progress, handleButtonClick, vertical }}152>153<div className={cn('relative', className)}>{children}</div>154</ProgressSliderContext.Provider>155);156};157158export const SliderContent: FC<SliderContentProps> = ({159children,160className,161}) => {162return <div className={cn('', className)}>{children}</div>;163};164165export const SliderWrapper: FC<SliderWrapperProps> = ({166children,167value,168className,169}) => {170const { active } = useProgressSliderContext();171172return (173<AnimatePresence mode='popLayout'>174{active === value && (175<motion.div176key={value}177initial={{ opacity: 0 }}178animate={{ opacity: 1 }}179exit={{ opacity: 0 }}180className={cn('', className)}181>182{children}183</motion.div>184)}185</AnimatePresence>186);187};188189export const SliderBtnGroup: FC<ProgressBarProps> = ({190children,191className,192}) => {193return <div className={cn('', className)}>{children}</div>;194};195196export const SliderBtn: FC<SliderBtnProps> = ({197children,198value,199className,200progressBarClass,201}) => {202const { active, progress, handleButtonClick, vertical } =203useProgressSliderContext();204205return (206<button207className={cn(208`relative ${active === value ? 'opacity-100' : 'opacity-50'}`,209className210)}211onClick={() => handleButtonClick(value)}212>213{children}214<div215className='absolute inset-0 overflow-hidden -z-10 max-h-full max-w-full '216role='progressbar'217aria-valuenow={active === value ? progress : 0}218>219<span220className={cn('absolute left-0 ', progressBarClass)}221style={{222[vertical ? 'height' : 'width']:223active === value ? `${progress}%` : '0%',224}}225/>226</div>227</button>228);229};
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | The content to be displayed within the slider. | |
duration | number | 5000 | The duration of the slider's transition in milliseconds. |
fastDuration | number | 400 | The duration of the slider's fast transition in milliseconds. |
vertical | boolean | false | Whether the slider is oriented vertically. |
activeSlider | string | The identifier of the currently active slider. | |
className | string | Optional CSS class for styling the slider component. |