ui
ui copied to clipboard
Carousel Component Feature Request
Not sure if this is the right place for a feature request but a great accessible carousel with keyboard controls, and good swipe support on mobile, would be awesome.
By the way, I love all these components so far, great job. The attention to detail on the design is top-notch.
I've done some research on how to approach this component with the good accessibility we used to see in Radix UI. There is a discussion about a future Radix carousel primitive so to keep following the best practices of this library, I think we have to wait until the component is finished and then integrate into shadcn/ui.
Also, if someone knows of another tiny library with good accesibility that we can integrate into this library and @shadcn approves, I'm willing to help.
We can use embla carousel for this
Great Request, would love to see this feature in shadcn/ui
Wow, I need just that! A carousel component and a slider component. There is one from Mantine UI but it lacks on customisation options.
https://ui.mantine.dev/category/carousels
Do you plan to add dot navigation? 🙏🏻
https://codesandbox.io/p/sandbox/embla-carousel-arrow-dots-react-l8k8jf?file=%2Fsrc%2Fjs%2Findex.tsx
Do you plan to add dot navigation? 🙏🏻
https://codesandbox.io/p/sandbox/embla-carousel-arrow-dots-react-l8k8jf?file=%2Fsrc%2Fjs%2Findex.tsx
![]()
You can quite easily add dot navigation using the api and button component, and if u want this to sit on top of the carousel just add it outside the CarouselItem:
{Array.from(Array(count).keys()).map((i) => (
<Button
key={i}
className={`mx-1 h-1.5 flex-grow rounded-full p-0 ${
i === current - 1
? "bg-white hover:bg-white"
: "bg-neutral-600/75"
}`}
onClick={() => api?.scrollTo(i)}
/>
))}
Hope this helps
Going to mark this as done. We'll figure out the dots navigation in another PR. Thanks everyone.
while this is closed here is a dots components you can just add to the carousel component in the ui folder following the structure of the existing components:
const CarouselDots = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const { api } = useCarousel();
const [updateState, setUpdateState] = React.useState(false);
const toggleUpdateState = React.useCallback(
() => setUpdateState((prevState) => !prevState),
[]
);
React.useEffect(() => {
if (api) {
api.on('select', toggleUpdateState);
api.on('reInit', toggleUpdateState);
return () => {
api.off('select', toggleUpdateState);
api.off('reInit', toggleUpdateState);
};
}
}, [api, toggleUpdateState]);
const numberOfSlides = api?.scrollSnapList().length || 0;
const currentSlide = api?.selectedScrollSnap() || 0;
if (numberOfSlides > 1) {
return (
<div ref={ref} className={`flex justify-center ${props.className}`}>
{Array.from({ length: numberOfSlides }, (_, i) => (
<Button
key={i}
className={`mx-1 h-1.5 w-1.5 rounded-full p-0 ${
i === currentSlide
? 'scale-125 transform bg-gray-500 hover:bg-gray-500'
: 'bg-gray-300 hover:bg-gray-300'
}`}
aria-label={`Go to slide ${i + 1}`}
onClick={() => api?.scrollTo(i)}
/>
))}
</div>
);
} else {
return <></>;
}
});
CarouselDots.displayName = 'CarouselDots';
If you're looking to add a dot indicator to your carousel, here's a simple way to do it. The idea is to maintain a state variable that keeps track of the current active index. Then, you can use this state variable to render the indicator dots and highlight the active one.
First, you'll need to set up your state variables:
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [count, setCount] = React.useState(0);
Then, in a useEffect hook, you can update the count and current state variables based on the api object:
React.useEffect(() => {
if (!api) {
return;
}
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
api.on("select", () => {
setCurrent(api.selectedScrollSnap() + 1);
});
}, [api]);
Finally, you can render the indicator dots and use the current state variable to highlight the active dot:
<Carousel setApi={setApi}>
<div className="flex justify-center mt-4">
{Array.from({ length: count }).map((_, index) => (
<span
key={index}
className={`inline-block w-3 h-3 rounded-full bg-gray-400 mx-2 ${
index + 1 === current ? "bg-[#FF407D]" : ""
}`}
onClick={() => api && api.scrollTo(index)}
/>
))}
</div>
<Carousel/>
You should also explore the api
there's a lot of things to do 👀
while this is closed here is a dots components you can just add to the carousel component in the ui folder following the structure of the existing components:
const CarouselDots = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >((props, ref) => { const { api } = useCarousel(); const [updateState, setUpdateState] = React.useState(false); const toggleUpdateState = React.useCallback( () => setUpdateState((prevState) => !prevState), [] ); React.useEffect(() => { if (api) { api.on('select', toggleUpdateState); api.on('reInit', toggleUpdateState); return () => { api.off('select', toggleUpdateState); api.off('reInit', toggleUpdateState); }; } }, [api, toggleUpdateState]); const numberOfSlides = api?.scrollSnapList().length || 0; const currentSlide = api?.selectedScrollSnap() || 0; if (numberOfSlides > 1) { return ( <div ref={ref} className={`flex justify-center ${props.className}`}> {Array.from({ length: numberOfSlides }, (_, i) => ( <Button key={i} className={`mx-1 h-1.5 w-1.5 rounded-full p-0 ${ i === currentSlide ? 'scale-125 transform bg-gray-500 hover:bg-gray-500' : 'bg-gray-300 hover:bg-gray-300' }`} aria-label={`Go to slide ${i + 1}`} onClick={() => api?.scrollTo(i)} /> ))} </div> ); } else { return <></>; } }); CarouselDots.displayName = 'CarouselDots';
Works great!
Added a PR for Carousal Dot components:
https://github.com/shadcn-ui/ui/pull/3320
while this is closed here is a dots components you can just add to the carousel component in the ui folder following the structure of the existing components:
const CarouselDots = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >((props, ref) => { const { api } = useCarousel(); const [updateState, setUpdateState] = React.useState(false); const toggleUpdateState = React.useCallback( () => setUpdateState((prevState) => !prevState), [] ); React.useEffect(() => { if (api) { api.on('select', toggleUpdateState); api.on('reInit', toggleUpdateState); return () => { api.off('select', toggleUpdateState); api.off('reInit', toggleUpdateState); }; } }, [api, toggleUpdateState]); const numberOfSlides = api?.scrollSnapList().length || 0; const currentSlide = api?.selectedScrollSnap() || 0; if (numberOfSlides > 1) { return ( <div ref={ref} className={`flex justify-center ${props.className}`}> {Array.from({ length: numberOfSlides }, (_, i) => ( <Button key={i} className={`mx-1 h-1.5 w-1.5 rounded-full p-0 ${ i === currentSlide ? 'scale-125 transform bg-gray-500 hover:bg-gray-500' : 'bg-gray-300 hover:bg-gray-300' }`} aria-label={`Go to slide ${i + 1}`} onClick={() => api?.scrollTo(i)} /> ))} </div> ); } else { return <></>; } }); CarouselDots.displayName = 'CarouselDots';
Works great!
Thanks for this! Hope your code is made a standard feature for shadcn's carousel component :)
while this is closed here is a dots components you can just add to the carousel component in the ui folder following the structure of the existing components:
const CarouselDots = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >((props, ref) => { const { api } = useCarousel(); const [updateState, setUpdateState] = React.useState(false); const toggleUpdateState = React.useCallback( () => setUpdateState((prevState) => !prevState), [] ); React.useEffect(() => { if (api) { api.on('select', toggleUpdateState); api.on('reInit', toggleUpdateState); return () => { api.off('select', toggleUpdateState); api.off('reInit', toggleUpdateState); }; } }, [api, toggleUpdateState]); const numberOfSlides = api?.scrollSnapList().length || 0; const currentSlide = api?.selectedScrollSnap() || 0; if (numberOfSlides > 1) { return ( <div ref={ref} className={`flex justify-center ${props.className}`}> {Array.from({ length: numberOfSlides }, (_, i) => ( <Button key={i} className={`mx-1 h-1.5 w-1.5 rounded-full p-0 ${ i === currentSlide ? 'scale-125 transform bg-gray-500 hover:bg-gray-500' : 'bg-gray-300 hover:bg-gray-300' }`} aria-label={`Go to slide ${i + 1}`} onClick={() => api?.scrollTo(i)} /> ))} </div> ); } else { return <></>; } }); CarouselDots.displayName = 'CarouselDots';
@casualzach You're the real MVP.