Skip to main content
react-md

Carousel

A carousel presents a set of items, referred to as slides, by sequentially displaying a subset of one or more slides. Typically, one slide is displayed at a time, and users can activate a next or previous slide control that hides the current slide and "rotates" the next or previous slide into view. In some implementations, rotation automatically starts when the page loads, and it may also automatically stop once all the slides have been displayed. While a slide may contain any type of content, image carousels where each slide contains nothing more than a single image are common.

See more about this pattern here.

This is an example of using the useCarousel hook and a few other components within react-md to create an accessible carousel. This demo was based off of the Image Carousel with Tabs example.

Slide 1 Title

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Slide 2 Title

Phasellus accumsan auctor neque, eu dignissim ex.

Slide 3 Title

Etiam vitae nisl ex. Maecenas ut elit risus.
import { Button } from "@react-md/core/button/Button";
import { Card } from "@react-md/core/card/Card";
import { CardContent } from "@react-md/core/card/CardContent";
import { objectFit } from "@react-md/core/objectFit";
import { ResponsiveItemOverlay } from "@react-md/core/responsive-item/ResponsiveItemOverlay";
import { Tab } from "@react-md/core/tabs/Tab";
import { TabList } from "@react-md/core/tabs/TabList";
import { Slide } from "@react-md/core/transition/Slide";
import { SlideContainer } from "@react-md/core/transition/SlideContainer";
import { useCarousel } from "@react-md/core/transition/useCarousel";
import { Typography } from "@react-md/core/typography/Typography";
import ChevronLeftIcon from "@react-md/material-icons/ChevronLeftIcon";
import ChevronRightIcon from "@react-md/material-icons/ChevronRightIcon";
import PauseIcon from "@react-md/material-icons/PauseIcon";
import PlayArrowIcon from "@react-md/material-icons/PlayArrowIcon";
import { type ReactElement, useId } from "react";

import styles from "./CarouselExample.module.scss";

/**
 * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/carousel/}
 * @see {@link https://www.w3.org/WAI/ARIA/apg/example-index/carousel/carousel-2-tablist.html}
 */
export default function CarouselExample(): ReactElement {
  const {
    paused,
    direction,
    activeIndex,
    increment,
    decrement,
    togglePaused,
    setActiveIndex,
  } = useCarousel({ totalSlides: slides.length });
  const id = useId();
  const carouselId = useId();

  return (
    <Card fullWidth className={styles.card}>
      <CardContent
        aria-label="Highlighted nature shots"
        aria-roledescription="carousel"
        id={id}
        role="region"
      >
        <SlideContainer
          aria-live="off"
          id={carouselId}
          direction={direction}
          className={styles.container}
        >
          {slides.map(({ src, title, subtitle }, index) => (
            <Slide
              aria-label={`Slide ${index + 1} of ${slides.length + 1}`}
              aria-roledescription="slide"
              id={`${carouselId}-${index + 1}`}
              role="group"
              key={title}
              active={activeIndex === index}
              timeout={500}
              className={styles.slide}
            >
              <img src={src} alt="" className={objectFit()} />
              <ResponsiveItemOverlay
                position="bottom"
                className={styles.overlay}
              >
                <Typography margin="none" type="headline-4" textAlign="center">
                  {title}
                </Typography>
                <Typography margin="none" type="headline-6" textAlign="center">
                  {subtitle}
                </Typography>
              </ResponsiveItemOverlay>
            </Slide>
          ))}
          <Button
            aria-label="Previous"
            disabled={activeIndex === 0}
            className={styles.control}
            buttonType="icon"
            onClick={decrement}
          >
            <ChevronLeftIcon />
          </Button>
          <Button
            aria-label="Next"
            disabled={activeIndex === slides.length - 1}
            buttonType="icon"
            className={styles.control}
            onClick={increment}
          >
            <ChevronRightIcon />
          </Button>
          <div className={styles.indicators}>
            <Button
              aria-label="Pause"
              aria-pressed={paused}
              buttonType="icon"
              onClick={togglePaused}
            >
              {paused ? <PlayArrowIcon /> : <PauseIcon />}
            </Button>
            <TabList
              activeIndex={activeIndex}
              setActiveIndex={setActiveIndex}
              className={styles.tablist}
            >
              {slides.map(({ title }, index) => (
                <Tab
                  key={title}
                  aria-label={`Slide ${index + 1}`}
                  aria-controls={`${carouselId}-${index + 1}`}
                  active={activeIndex === index}
                  className={styles.indicator}
                />
              ))}
            </TabList>
          </div>
        </SlideContainer>
      </CardContent>
    </Card>
  );
}

interface CarouselSlide {
  src: string;
  title: string;
  subtitle: string;
}

const slides: readonly CarouselSlide[] = [
  {
    src: "https://picsum.photos/640/360?image=803",
    title: "Slide 1 Title",
    subtitle: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
  },
  {
    src: "https://picsum.photos/640/360?image=852",
    title: "Slide 2 Title",
    subtitle: "Phasellus accumsan auctor neque, eu dignissim ex.",
  },
  {
    src: "https://picsum.photos/640/360?image=902",
    title: "Slide 3 Title",
    subtitle: "Etiam vitae nisl ex. Maecenas ut elit risus.",
  },
];

Press Enter to start editing.

@use "everything";

.card {
  @include everything.button-set-var(color, everything.$white);

  max-width: 42rem;
}

.container {
  position: relative;
}

.control {
  border-radius: 0;
  bottom: 0;
  height: 100%;
  position: absolute;
  top: 0;
  width: 5rem;

  &:nth-of-type(2) {
    right: 0;
  }
}

.slide {
  @include everything.transition-set-var(slide-duration, 0.5s);

  height: 22.5rem;
  overflow: hidden;
}

.overlay {
  padding-bottom: 4rem;
}

.indicators {
  align-items: center;
  bottom: 0;
  display: flex;
  gap: 0.25rem;
  justify-content: center;
  left: 0;
  padding: 1rem;
  padding-bottom: 0.25rem;
  position: absolute;
  right: 0;
}

.tablist {
  gap: 0.25rem;
  width: auto;
}

.indicator {
  background-color: rgba(everything.$white, 0.54);
  min-height: 0.75rem;
  min-width: 3rem;
  width: 3rem;

  &[aria-selected="true"] {
    background-color: everything.$white;
  }
}

Press Enter to start editing.