All components
Progressive Carousel
carouselsUi-Layouts component.
responsive · 560px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/progressive-carousel.jsonUsage
'use client';
import {
ProgressSlider,
SliderBtn,
SliderBtnGroup,
SliderContent,
SliderWrapper,
} from '@/registry/ui-layouts/progressive-carousel';
import React from 'react';
const SLIDES = [
{
value: 'slide-1',
img: 'https://picsum.photos/seed/pc1/900/500',
label: 'Mountain Vista',
sub: 'Snow-capped peaks at golden hour',
},
{
value: 'slide-2',
img: 'https://picsum.photos/seed/pc2/900/500',
label: 'Ocean Breeze',
sub: 'Endless horizon over turquoise water',
},
{
value: 'slide-3',
img: 'https://picsum.photos/seed/pc3/900/500',
label: 'Forest Path',
sub: 'Ancient trees filtered in morning light',
},
{
value: 'slide-4',
img: 'https://picsum.photos/seed/pc4/900/500',
label: 'Desert Dunes',
sub: 'Rolling sand at twilight',
},
];
export default function Demo() {
return (
<div className='w-full max-w-4xl mx-auto px-4 py-6'>
<ProgressSlider activeSlider='slide-1' duration={4000} className='flex flex-col gap-4'>
{/* Main image display */}
<SliderContent className='relative w-full overflow-hidden rounded-xl aspect-video bg-muted'>
{SLIDES.map((s) => (
<SliderWrapper key={s.value} value={s.value} className='absolute inset-0'>
<img
src={s.img}
alt={s.label}
className='w-full h-full object-cover'
/>
<div className='absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent text-white'>
<p className='text-lg font-semibold'>{s.label}</p>
<p className='text-sm opacity-80'>{s.sub}</p>
</div>
</SliderWrapper>
))}
</SliderContent>
{/* Navigation buttons with progress bars */}
<SliderBtnGroup className='flex gap-2 w-full'>
{SLIDES.map((s) => (
<SliderBtn
key={s.value}
value={s.value}
className='flex-1 h-1 rounded-full bg-muted-foreground/20 cursor-pointer'
progressBarClass='h-full bg-foreground rounded-full top-0'
/>
))}
</SliderBtnGroup>
</ProgressSlider>
</div>
);
}Component source
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'motion/react';
import React, {
createContext,
type FC,
type ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
// Define the type for the context value
interface ProgressSliderContextType {
active: string;
progress: number;
handleButtonClick: (value: string) => void;
vertical: boolean;
}
// Define the type for the component props
interface ProgressSliderProps {
children: ReactNode;
duration?: number;
fastDuration?: number;
vertical?: boolean;
activeSlider: string;
className?: string;
}
interface SliderContentProps {
children: ReactNode;
className?: string;
}
interface SliderWrapperProps {
children: ReactNode;
value: string;
className?: string;
}
interface ProgressBarProps {
children: ReactNode;
className?: string;
}
interface SliderBtnProps {
children: ReactNode;
value: string;
className?: string;
progressBarClass?: string;
}
// Create the context with an undefined initial value
const ProgressSliderContext = createContext<ProgressSliderContextType | undefined>(undefined);
export const useProgressSliderContext = (): ProgressSliderContextType => {
const context = useContext(ProgressSliderContext);
if (!context) {
throw new Error('useProgressSliderContext must be used within a ProgressSlider');
}
return context;
};
export const ProgressSlider: FC<ProgressSliderProps> = ({
children,
duration = 5000,
fastDuration = 400,
vertical = false,
activeSlider,
className,
}) => {
const [active, setActive] = useState<string>(activeSlider);
const [progress, setProgress] = useState<number>(0);
const [isFastForward, setIsFastForward] = useState<boolean>(false);
const frame = useRef<number>(0);
const firstFrameTime = useRef<number>(performance.now());
const targetValue = useRef<string | null>(null);
const [sliderValues, setSliderValues] = useState<string[]>([]);
useEffect(() => {
const getChildren = React.Children.toArray(children).find(
(child) => (child as React.ReactElement<any>).type === SliderContent
) as React.ReactElement<any> | undefined;
if (getChildren) {
const values = React.Children.toArray(getChildren.props.children).map(
(child) => (child as React.ReactElement<any>).props.value as string
);
setSliderValues(values);
}
}, [children]);
useEffect(() => {
if (sliderValues.length > 0) {
firstFrameTime.current = performance.now();
frame.current = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(frame.current);
};
}, [sliderValues, active, isFastForward]);
const animate = (now: number) => {
const currentDuration = isFastForward ? fastDuration : duration;
const elapsedTime = now - firstFrameTime.current;
const timeFraction = elapsedTime / currentDuration;
if (timeFraction <= 1) {
setProgress(isFastForward ? progress + (100 - progress) * timeFraction : timeFraction * 100);
frame.current = requestAnimationFrame(animate);
} else {
if (isFastForward) {
setIsFastForward(false);
if (targetValue.current !== null) {
setActive(targetValue.current);
targetValue.current = null;
}
} else {
// Move to the next slide
const currentIndex = sliderValues.indexOf(active);
const nextIndex = (currentIndex + 1) % sliderValues.length;
setActive(sliderValues[nextIndex]);
}
setProgress(0);
firstFrameTime.current = performance.now();
}
};
const handleButtonClick = (value: string) => {
if (value !== active) {
const elapsedTime = performance.now() - firstFrameTime.current;
const currentProgress = (elapsedTime / duration) * 100;
setProgress(currentProgress);
targetValue.current = value;
setIsFastForward(true);
firstFrameTime.current = performance.now();
}
};
return (
<ProgressSliderContext.Provider value={{ active, progress, handleButtonClick, vertical }}>
<div className={cn('relative', className)}>{children}</div>
</ProgressSliderContext.Provider>
);
};
export const SliderContent: FC<SliderContentProps> = ({ children, className }) => {
return <div className={cn('', className)}>{children}</div>;
};
export const SliderWrapper: FC<SliderWrapperProps> = ({ children, value, className }) => {
const { active } = useProgressSliderContext();
return (
<AnimatePresence mode='popLayout'>
{active === value && (
<motion.div
key={value}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn('', className)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
export const SliderBtnGroup: FC<ProgressBarProps> = ({ children, className }) => {
return <div className={cn('', className)}>{children}</div>;
};
export const SliderBtn: FC<SliderBtnProps> = ({ children, value, className, progressBarClass }) => {
const { active, progress, handleButtonClick, vertical } = useProgressSliderContext();
return (
<button
className={cn(`relative ${active === value ? 'opacity-100' : 'opacity-50'}`, className)}
onClick={() => handleButtonClick(value)}
>
{children}
<div
className='absolute inset-0 overflow-hidden -z-10 max-h-full max-w-full '
role='progressbar'
aria-valuenow={active === value ? progress : 0}
>
<span
className={cn('absolute left-0 ', progressBarClass)}
style={{
[vertical ? 'height' : 'width']: active === value ? `${progress}%` : '0%',
}}
/>
</div>
</button>
);
};Dependencies
motion
Source: Ui-Layouts