ui icon indicating copy to clipboard operation
ui copied to clipboard

Carousel Component Feature Request

Open caleb654 opened this issue 1 year ago • 6 comments

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.

caleb654 avatar May 10 '23 20:05 caleb654

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.

jsm94 avatar May 17 '23 10:05 jsm94

We can use embla carousel for this

its-monotype avatar May 17 '23 16:05 its-monotype

Great Request, would love to see this feature in shadcn/ui

FleetAdmiralJakob avatar Jun 08 '23 15:06 FleetAdmiralJakob

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

Vintotan avatar Jul 11 '23 20:07 Vintotan

Do you plan to add dot navigation? 🙏🏻

https://codesandbox.io/p/sandbox/embla-carousel-arrow-dots-react-l8k8jf?file=%2Fsrc%2Fjs%2Findex.tsx

image

dninomiya avatar Dec 24 '23 07:12 dninomiya

Do you plan to add dot navigation? 🙏🏻

https://codesandbox.io/p/sandbox/embla-carousel-arrow-dots-react-l8k8jf?file=%2Fsrc%2Fjs%2Findex.tsx

image

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

EggsLeggs avatar Dec 25 '23 12:12 EggsLeggs

Going to mark this as done. We'll figure out the dots navigation in another PR. Thanks everyone.

shadcn avatar Jan 14 '24 08:01 shadcn

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 avatar Feb 21 '24 19:02 casualzach

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 👀

7hourspg avatar Mar 01 '24 05:03 7hourspg

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!

684efs3 avatar Mar 23 '24 14:03 684efs3

Added a PR for Carousal Dot components:

https://github.com/shadcn-ui/ui/pull/3320

grvv avatar Apr 02 '24 18:04 grvv

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 :)

sohaibfurqan92 avatar Apr 06 '24 13:04 sohaibfurqan92

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.

gary-lo avatar May 01 '24 04:05 gary-lo