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.
Carousel Example
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.