Carousel

Our Carousel component supports the following features:

  • Makes use of the embla-carousel-react library to handle the carousel logic. The <CarouselRoot /> component accepts all options that the Embla library accepts.
  • Meets accessibility standards.
  • Supports any width of slide including variable width slides.
  • Mouse and touchscreen slide navigation.
  • Slide styling and behaviour can be customised based on wether the slide is active or visible.
  • Faded edges can be added to the left and right of the carousel.
  • Nav buttons can be positioned anywhere in the carousel (although we recommend them above the carousel for full a11y compliance).

Examples

Navigation buttons positioning

For sites that must comply with WCAG AA standards, the navigation buttons must be positioned above the carousel, and must appear before the carousel in the DOM. This is so that screen reader users can navigate to a slide and then view that slide, without having to navigate backwards.

<CarouselRoot aria-label="An example carousel with multiple image slides">
  <PageWidth width="skinny" className="mb-2 flex justify-end gap-2">
    <CarouselPrevButton />
    <CarouselNextButton />
  </PageWidth>
  <PageWidth width="skinny">
    <CarouselWindow className="rounded-xl">
      <Carousel>
        <CarouselTrack>
          {images.map((image, index) => (
            <CarouselItem
              key={index}
              className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] lg:w-[calc(33.33%-2.5rem)]"
            >
              <Image
                src={image}
                alt={image.alt}
                className="aspect-[3/2] rounded-xl object-cover md:aspect-square lg:aspect-[2/3]"
              />
            </CarouselItem>
          ))}
        </CarouselTrack>
      </Carousel>
    </CarouselWindow>
  </PageWidth>
</CarouselRoot>

Full width

Carousel items can be styled with Tailwind classes. You can set each slide to take up the full carousel width by applying className="w-full" to the <CarouselItem /> component.

<PageWidth width="skinny">
  <CarouselRoot aria-label="An example carousel with single full width image slides">
    <div className="mb-2 flex justify-end gap-2">
      <CarouselPrevButton />
      <CarouselNextButton />
    </div>
    <CarouselWindow className="rounded-xl">
      <Carousel>
        <CarouselTrack>
          {images.map((image, index) => (
            <CarouselItem key={index} className="w-full">
              <Image
                src={image}
                alt={image.alt}
                className="aspect-[3/2] rounded-xl object-cover md:aspect-[5/2]"
              />
            </CarouselItem>
          ))}
        </CarouselTrack>
      </Carousel>
    </CarouselWindow>
  </CarouselRoot>
</PageWidth>

Faded edges

Faded edges can be added to the left and right of the carousel.

Important: The two <CarouselFadedEdge /> components must be wrapped in a div with className="relative" to work correctly.

<CarouselRoot aria-label="An example carousel with faded edges">
  <PageWidth width="skinny" className="mb-2 flex justify-end gap-2">
    <CarouselPrevButton />
    <CarouselNextButton />
  </PageWidth>
  <CarouselWindow>
    <PageWidth width="skinny" className="relative">
      <CarouselFadedEdge side="left" />
      <CarouselFadedEdge side="right" />
      <Carousel>
        <CarouselTrack>
          {images.map((image, index) => (
            <CarouselItem
              key={index}
              className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] lg:w-[calc(33.33%-2.5rem)]"
            >
              <Image
                src={image}
                alt={image.alt}
                className="aspect-[3/2] rounded-xl object-cover md:aspect-square lg:aspect-[2/3]"
              />
            </CarouselItem>
          ))}
        </CarouselTrack>
      </Carousel>
    </PageWidth>
  </CarouselWindow>
</CarouselRoot>

Articles

Carousel items are agnostic to their content. You may use the carousel to render a list of articles.

<CarouselRoot aria-label="An example carousel with multiple article slides">
  <PageWidth width="skinny" className="mb-2 flex justify-end gap-2">
    <CarouselPrevButton />
    <CarouselNextButton />
  </PageWidth>
  <PageWidth width="skinny">
    <CarouselWindow className="rounded-xl">
      <Carousel>
        <CarouselTrack>
          {articles.map((article) => (
            <CarouselItem key={article.id} className="w-80">
              <Article {...article} />
            </CarouselItem>
          ))}
        </CarouselTrack>
      </Carousel>
    </CarouselWindow>
  </PageWidth>
</CarouselRoot>

Slide Context

In this example, each slide is a video group. The slide component uses the useCarouselItemContext() hook to access the context of the carousel item. It then uses that information to apply an animated styling to the inactive slides and to play/pause the video as the slides become active/inactive.

const VideoSlide = ({ video }: { video: Video }) => {
  const { isActive, slideIndex, activeSlideIndex } = useCarouselItemContext();
  const { ref, play, pause, isPlaying } = useVideoPlayer();
 
  useEffect(() => {
    if (isActive) {
      play();
    } else {
      pause();
    }
  }, [isActive, play, pause]);
 
  return (
    <div
      className={cn(
        'relative transition-all duration-300',
        slideIndex < activeSlideIndex && 'origin-right scale-90 opacity-50',
        slideIndex > activeSlideIndex && 'origin-left scale-90 opacity-50',
      )}
    >
      <Video ref={ref} src={video.src} />
      <button onClick={isPlaying ? pause : play}>
        {isPlaying ? (
          <RiPauseLine className="h-4 w-4" />
        ) : (
          <RiPlayFill className="h-4 w-4" />
        )}
      </button>
    </div>
  );
};
 
const VideoCarousel = () => {
  return (
    <CarouselRoot aria-label="An example carousel with cool animated video slides">
      <PageWidth width="skinny" className="mb-2 flex justify-end gap-2">
        <CarouselPrevButton />
        <CarouselNextButton />
      </PageWidth>
      <CarouselWindow>
        <PageWidth width="skinny" className="relative">
          <CarouselFadedEdge side="left" />
          <CarouselFadedEdge side="right" />
          <Carousel>
            <CarouselTrack>
              {videos.map((video, index) => (
                <CarouselItem key={index} className="w-full">
                  <VideoSlide video={video} />
                </CarouselItem>
              ))}
            </CarouselTrack>
          </Carousel>
        </PageWidth>
      </CarouselWindow>
    </CarouselRoot>
  );
};

Requirements

The following libraries need to be installed:

3rd Party Librariesversion
embla-carousel-react9.0.0-rc01

The following folders need to be copied to your project:

Copy to projectReason
ui/components/carousel/Carousel components and hooks
ui/structure/page-width/Useful for setting the width bounds of the carousel

Components

<CarouselRoot />

Required at the top level of the carousel. It provides all the context and logic for the carousel.

Props
PropTypeDescription
@extendsComponentProps<'section'>All props from the <section> element are accepted.
optionsEmblaOptionsUsed to configure the underlying Embla carousel instance.
pluginsEmblaPlugin[]Add plugins to the underlying Embla carousel instance.
controllerCarouselControllerIf you want to use a custom carousel controller, you can pass it here. This is in case you need access to the carousel state outside of the <CarouselRoot /> component.

<CarouselWindow />

An overflow-hidden container that contains** **the carousel. In the standard example this sits inside a <PageWidth /> component so that the visual boundary of the carousel is confined to the page width.

If however, parts of the non-active slides are visible outside of the page width (see faded edges example), the <CarouselWindow /> should be expanded to allow for the additional width.

Props
PropTypeDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.

An aria-live container that contains the carousel. Slides that are outside the boundary of this component are considered "not visible".

Props
PropTypeDescription
@extendsComponentProps<'section'>All props from the <div> element are accepted.

<CarouselTrack />

The track element that slides left to right.

Props
PropTypeDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.

<CarouselItem />

An individual slide container. When outside the boundaries of the <Carousel /> component, the slide is considered "not visible" and given an inert attribute so that it becomes non-interactive and is hidden from screen readers.

Props
PropTypeDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.
Data attributes
Data attributeTypeDescription
data-slide-indexnumberThe index of the slide.
data-visiblebooleanWhether the slide is visible.
data-activebooleanWhether the slide is active.

<CarouselPrevButton />

The previous navigation button.

Props
PropTypeDescription
@extendsComponentProps<'button'>All props from the <button> element are accepted.

<CarouselNextButton />

The next navigation button.

Props
PropTypeDescription
@extendsComponentProps<'button'>All props from the <button> element are accepted.

<CarouselFadedEdge />

An empty div that adds a nice faded efect to the edge of the carousel. It must be added inside an element with poosition: relative.

Props
PropTypeDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.
side'left' | 'right'The side of the edge to apply the faded effect to.

hooks

useCarouselContext()

Call from inside the <CarouselRoot /> component. Returns the context of the carousel.

Returns
PropertyTypeDescription
controllerCarouselControllerThe controller of the carousel.

useCarouselItemContext()

Call from inside an individual Carousel Item. Returns the context of the carousel item.

Returns
PropertyTypeDescription
carouselIdstringThe id of the carousel.
slideIndexnumberThe index of the current slide.
isVisiblebooleanWhether the slide is visible.
isActivebooleanWhether the slide is active.
activeSlideIndexnumberThe index of the active slide.