Progressive Carousel

An animated progressive carousel built with Framer Motion, offering smooth and dynamic transitions between items.

A breathtaking view of a city illuminated by countless lights, showcasing the vibrant and bustling nightlife.

Installation

npm install framer-motion
progress-slider.tsx
1
import React, {
2
createContext,
3
useContext,
4
useState,
5
useEffect,
6
useRef,
7
ReactNode,
8
FC,
9
} from 'react';
10
import { motion, AnimatePresence } from 'framer-motion';
11
import { cn } from '@/lib/utils';
12
13
// Define the type for the context value
14
interface ProgressSliderContextType {
15
active: string;
16
progress: number;
17
handleButtonClick: (value: string) => void;
18
vertical: boolean;
19
}
20
21
// Define the type for the component props
22
interface ProgressSliderProps {
23
children: ReactNode;
24
duration?: number;
25
fastDuration?: number;
26
vertical?: boolean;
27
activeSlider: string;
28
className?: string;
29
}
30
31
interface SliderContentProps {
32
children: ReactNode;
33
className?: string;
34
}
35
36
interface SliderWrapperProps {
37
children: ReactNode;
38
value: string;
39
className?: string;
40
}
41
42
interface ProgressBarProps {
43
children: ReactNode;
44
className?: string;
45
}
46
47
interface SliderBtnProps {
48
children: ReactNode;
49
value: string;
50
className?: string;
51
progressBarClass?: string;
52
}
53
54
// Create the context with an undefined initial value
55
const ProgressSliderContext = createContext<
56
ProgressSliderContextType | undefined
57
>(undefined);
58
59
export const useProgressSliderContext = (): ProgressSliderContextType => {
60
const context = useContext(ProgressSliderContext);
61
if (!context) {
62
throw new Error(
63
'useProgressSliderContext must be used within a ProgressSlider'
64
);
65
}
66
return context;
67
};
68
69
export const ProgressSlider: FC<ProgressSliderProps> = ({
70
children,
71
duration = 5000,
72
fastDuration = 400,
73
vertical = false,
74
activeSlider,
75
className,
76
}) => {
77
const [active, setActive] = useState<string>(activeSlider);
78
const [progress, setProgress] = useState<number>(0);
79
const [isFastForward, setIsFastForward] = useState<boolean>(false);
80
const frame = useRef<number>(0);
81
const firstFrameTime = useRef<number>(performance.now());
82
const targetValue = useRef<string | null>(null);
83
const [sliderValues, setSliderValues] = useState<string[]>([]);
84
85
useEffect(() => {
86
const getChildren = React.Children.toArray(children).find(
87
(child) => (child as React.ReactElement).type === SliderContent
88
) as React.ReactElement | undefined;
89
90
if (getChildren) {
91
const values = React.Children.toArray(getChildren.props.children).map(
92
(child) => (child as React.ReactElement).props.value as string
93
);
94
setSliderValues(values);
95
}
96
}, [children]);
97
98
useEffect(() => {
99
if (sliderValues.length > 0) {
100
firstFrameTime.current = performance.now();
101
frame.current = requestAnimationFrame(animate);
102
}
103
return () => {
104
cancelAnimationFrame(frame.current);
105
};
106
}, [sliderValues, active, isFastForward]);
107
108
const animate = (now: number) => {
109
const currentDuration = isFastForward ? fastDuration : duration;
110
const elapsedTime = now - firstFrameTime.current;
111
const timeFraction = elapsedTime / currentDuration;
112
113
if (timeFraction <= 1) {
114
setProgress(
115
isFastForward
116
? progress + (100 - progress) * timeFraction
117
: timeFraction * 100
118
);
119
frame.current = requestAnimationFrame(animate);
120
} else {
121
if (isFastForward) {
122
setIsFastForward(false);
123
if (targetValue.current !== null) {
124
setActive(targetValue.current);
125
targetValue.current = null;
126
}
127
} else {
128
// Move to the next slide
129
const currentIndex = sliderValues.indexOf(active);
130
const nextIndex = (currentIndex + 1) % sliderValues.length;
131
setActive(sliderValues[nextIndex]);
132
}
133
setProgress(0);
134
firstFrameTime.current = performance.now();
135
}
136
};
137
138
const handleButtonClick = (value: string) => {
139
if (value !== active) {
140
const elapsedTime = performance.now() - firstFrameTime.current;
141
const currentProgress = (elapsedTime / duration) * 100;
142
setProgress(currentProgress);
143
targetValue.current = value;
144
setIsFastForward(true);
145
firstFrameTime.current = performance.now();
146
}
147
};
148
149
return (
150
<ProgressSliderContext.Provider
151
value={{ active, progress, handleButtonClick, vertical }}
152
>
153
<div className={cn('relative', className)}>{children}</div>
154
</ProgressSliderContext.Provider>
155
);
156
};
157
158
export const SliderContent: FC<SliderContentProps> = ({
159
children,
160
className,
161
}) => {
162
return <div className={cn('', className)}>{children}</div>;
163
};
164
165
export const SliderWrapper: FC<SliderWrapperProps> = ({
166
children,
167
value,
168
className,
169
}) => {
170
const { active } = useProgressSliderContext();
171
172
return (
173
<AnimatePresence mode='popLayout'>
174
{active === value && (
175
<motion.div
176
key={value}
177
initial={{ opacity: 0 }}
178
animate={{ opacity: 1 }}
179
exit={{ opacity: 0 }}
180
className={cn('', className)}
181
>
182
{children}
183
</motion.div>
184
)}
185
</AnimatePresence>
186
);
187
};
188
189
export const SliderBtnGroup: FC<ProgressBarProps> = ({
190
children,
191
className,
192
}) => {
193
return <div className={cn('', className)}>{children}</div>;
194
};
195
196
export const SliderBtn: FC<SliderBtnProps> = ({
197
children,
198
value,
199
className,
200
progressBarClass,
201
}) => {
202
const { active, progress, handleButtonClick, vertical } =
203
useProgressSliderContext();
204
205
return (
206
<button
207
className={cn(
208
`relative ${active === value ? 'opacity-100' : 'opacity-50'}`,
209
className
210
)}
211
onClick={() => handleButtonClick(value)}
212
>
213
{children}
214
<div
215
className='absolute inset-0 overflow-hidden -z-10 max-h-full max-w-full '
216
role='progressbar'
217
aria-valuenow={active === value ? progress : 0}
218
>
219
<span
220
className={cn('absolute left-0 ', progressBarClass)}
221
style={{
222
[vertical ? 'height' : 'width']:
223
active === value ? `${progress}%` : '0%',
224
}}
225
/>
226
</div>
227
</button>
228
);
229
};

Props

PropTypeDefaultDescription
childrenReactNodeThe content to be displayed within the slider.
durationnumber5000The duration of the slider's transition in milliseconds.
fastDurationnumber400The duration of the slider's fast transition in milliseconds.
verticalbooleanfalseWhether the slider is oriented vertically.
activeSliderstringThe identifier of the currently active slider.
classNamestringOptional CSS class for styling the slider component.

Example

A breathtaking view of a city illuminated by countless lights, showcasing the vibrant and bustling nightlife.