Theme studio

Design a theme, preview it live, then export it. Saved in this browser.

Quick picks #10b981
Generated scale
50100200300400500600700800900
Some text is below AA

Carousel

  • Stable
  • WCAG 2.2 evidence
  • RTL

A carousel: slides in a scroll-snap strip with Previous / Next, indicators and, when you opt in, automatic rotation with a visible pause button. It composes CarouselControls and CarouselIndicator; the container has no Figma node (decision D8: no React Aria primitive, the WAI-ARIA carousel pattern). Structure and names (WCAG 1.3.1, 4.1.2): a region (or group) named by you with aria-roledescription="carousel"; the controls first, then the slides, then the indicators; each CarouselSlide is a group with aria-roledescription="slide" and a name like "3 of 8". The controls and indicators point at the slide strip with aria-controls. Slides out of view are inert and aria-hidden, so focus and screen readers only meet what is on screen (2.4.3). Keyboard (2.1.1): Previous, Next, the rotation button and the indicators are buttons (Tab, Enter, Space). The slide strip is a scrollable region, and a scrollable region with nothing focusable in it cannot be reached or scrolled by keyboard on its own (automated checks such as axe's scrollable-region-focusable flag it). So the strip is a tab stop only while the slides in view hold nothing focusable: Arrow keys (mirrored in right-to-left layouts), Home and End then move between slides. Where the visible slide has a link or a button, that is the focusable content, the strip stays out of the tab order and no extra Tab stop is added. If focus is inside a slide that is about to go out of view, focus moves to the strip instead of being lost. Motion (2.2.2, 2.3.3): rotation is off unless autoPlay, always comes with a visible Stop / Start button, pauses while the pointer is over the carousel or focus is inside it, never starts by itself with prefers-reduced-motion (and stops if the preference appears), and scrolls without animation then. While rotating the strip is aria-live="off", otherwise polite, so a change of slide is announced only when the user caused it. Direction (1.3.2): the strip, Previous / Next and the keys follow the reading direction. Layout: the carousel is a size container; slidesPerView can change with its width. Controls and indicators are hidden when there is only one position. Server rendering outputs every slide, the first one in view, without any measurement. Consumer duties: a specific name, text alternatives of the pictures (Image's alt), enough time per slide, and a link to every slide's content that does not depend on the carousel when the content matters (carousels hide most of what they hold). Focusable slide content should use an inset focus ring where it touches the slide edge: the strip clips overflow.

import { Carousel } from "@nexera-ui/react";
Loading example…
8
examples
22
props
8
live controls
1
platform
9
WCAG criteria
3
blocks use it

Try every prop. Copy the code.

Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.

Loading…
children
role
isLooping
autoPlay
isPlaying
defaultPlaying
indicator
surface
Generated code
import { Carousel } from "@nexera-ui/react";

<Carousel />

Examples 7

The same examples as Storybook, rendered live. Open Code to copy one.

With images

Pictures with the controls and the indicator laid over them (surface="on-media"), looping. The slides are Images: each has its own alternative text, so a screen reader hears the picture and "3 of 6".

Loading example…

Auto play

Opt-in automatic rotation (autoPlay, interval). The Stop / Start button is always there (WCAG 2.2.2); rotation also pauses while the pointer is over the carousel or focus is inside it, and never starts by itself when the operating system asks for reduced motion. While it rotates the slide strip is aria-live="off", otherwise polite.

Loading example…

Multiple slides per view

Several slides in view: slidesPerView takes a number or one number per width of the carousel's own container (here 1, then 2 from 480 px, then 3 from 1024 px). The slides are ImageTile buttons, so they are the focusable content and the strip itself is not a tab stop. One position per slide that can start the view, so there are fewer dots than tiles.

Loading example…

Counter

The counter indicator ("2 / 6", read as "Slide 2 of 6"): one Tab stop fewer per slide in a long carousel.

Loading example…

Controlled

Controlled index and onIndexChange: the position lives in the parent, which can move it too.

Loading example…

RTL

Right to left: the strip scrolls from the right, Previous points right, the indicators and the arrow keys follow the reading direction, and the names are translated (labels, getSlideLabel, getIndicatorLabel).

Loading example…

Narrow container

Container-based, not viewport-based: the same slidesPerView setting shows one slide in a 240 px column and two in a wide one. Many slides make the indicators wrap instead of overflowing (WCAG 1.4.10).

Loading example…

Props 22

Press "Try it" on a card to load that prop into the playground.

22 props shown

children*Nexera
ReactNode

The slides: CarouselSlide elements, each one a direct child (fragments are not unpacked). The number of children is the number of slides; null, false and undefined do not count.

Default –
roleNexera
"region" | "group"

Role of the root: region (a landmark that appears in landmark lists, for a carousel that is a main part of the page) or group (for a carousel inside other content). Either way it is announced as a "carousel".

Default region
slidesPerViewNexera
CarouselSlidesPerView

Slides in view at once: a whole number of at least 1 (smaller or fractional values are corrected), or an object with one number per container width ({ base: 1, sm: 2, lg: 3 }, see CarouselResponsiveSlides). The widths are those of the carousel's own container (@nx-sm and up), not the viewport. Previous and Next move one slide at a time and the last position is the one that shows the last slide in the last place. The CSS follows the container without JavaScript. The server renders (and the first client render uses) the base number for which slides are inert; after hydration the container is measured and the other numbers apply.

Default 1
indexNexera
number

Position of the first slide in view, from 0 (controlled). Pair it with onIndexChange. With several slides per view the highest position is slides - slidesPerView.

Default –
defaultIndexNexera
number

Position at first render (uncontrolled). The server output always starts at the first slide; a higher value scrolls into place when the page hydrates.

Default 0
onIndexChangeNexera
(index: number) => void

Called with the new position when it changes: Previous, Next, an indicator, the arrow keys on the focused slide strip, a swipe or scroll that settles on another slide, or the automatic rotation.

Default –
isLoopingNexera
boolean

Wrap around: Next on the last position goes back to the first, Previous on the first goes to the last, and the automatic rotation starts over. The strip scrolls back; slides are not cloned, so assistive technology never meets a duplicate. Without it the ends are aria-disabled and the automatic rotation stops at the last slide.

Default false
autoPlayNexera
boolean

Rotate slides automatically (opt in). Adds the rotation button (Stop / Start automatic slide show, WCAG 2.2.2) next to Previous and Next. Rotation pauses while the pointer is over the carousel or keyboard focus is inside it (the rotation button itself excepted, so pressing Start works), and does not start by itself when the user prefers reduced motion; the button is still there to start it deliberately. The slide strip is aria-live="off" while rotating and polite when not, as the WAI-ARIA carousel pattern asks.

Default false
intervalNexera
number

Time each slide stays in view while rotating, in milliseconds. Pressing Previous, Next or an indicator restarts the count. Give people time to read: allow for the slide's text.

Default 5000
isPlayingNexera
boolean

Whether the slides rotate right now (controlled), shown by the rotation button. Only with autoPlay. Pair it with onPlayingChange.

Default –
defaultPlayingNexera
boolean

Rotation state at first render (uncontrolled), only with autoPlay. Pass false to wait for the user to press Start. Reduced motion starts paused whatever this says.

Default true
onPlayingChangeNexera
(isPlaying: boolean) => void

Called with the new rotation state when the user presses the rotation button, when the preference for reduced motion stops rotation, and when rotation stops at the last slide of a carousel that is not looping.

Default –
indicatorNexera
"dots" | "lines" | "counter" | "none"

Which indicator shows the position: dots, lines or counter ("2 / 5", text, not interactive), or none. One indicator per position, so with several slides per view there are fewer than slides. All but counter are buttons and one Tab stop each; counter or none keep the tab order short in long carousels.

Default dots
surfaceNexera
"on-light" | "on-media"

Background the controls and the indicator sit on: on-light puts them above and below the slides; on-media lays them over the bottom of the slides (white marks and buttons, for pictures that stay dark enough behind them: check the contrast, WCAG 1.4.11).

Default on-light
labelsNexera
Partial<CarouselLabels>

Accessible names of the controls, the slide strip and the indicators; English defaults. Translate them.

Default –
getSlideLabelNexera
(index: number, count: number) => string

Name of each slide (WCAG 4.1.2), "3 of 8" in English. A slide's own aria-label wins. Translate it.

Default (index: number, count: number) => `${String(index + 1)} of ${String(count)}`
getIndicatorLabelNexera
(index: number, count: number) => string

Name of each indicator button; index is the position it goes to and count the number of slides. Translate it.

Default (index: number, count: number) => `Go to slide ${String(index + 1)} of ${String(count)}`
getCounterLabelNexera
(index: number, count: number) => string

Text read by assistive technology for indicator="counter". Translate it.

Default (index: number, count: number) => `Slide ${String(index + 1)} of ${String(count)}`
classNameNexera
string

Extra classes for the root, merged last so they win over the defaults.

Default –
styleNexera
CSSProperties

Inline style for the root.

Default –
aria-labelNexera
string

Accessible name of the carousel ("Team photos"). Required unless aria-labelledby is set. Translate it. Accessible name of the carousel. Optional when aria-labelledby is set.

Default –
aria-labelledbyNexera
string

Id(s) of visible element(s) that name the carousel; wins over aria-label when both are set. Id(s) of visible element(s) that name the carousel (for example its heading). Required unless aria-label is set.

Default –

* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.

Accessible by default.

Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.

WCAG 2.2 evidence

7 direct · 2 supporting
  • 1.3.1Info and RelationshipsLevel A · tested directly
  • 1.3.2Meaningful SequenceLevel A · tested directly
  • 1.4.10ReflowLevel AA · supporting test
  • 2.1.1KeyboardLevel A · tested directly
  • 2.2.2Pause, Stop, HideLevel A · tested directly
  • 2.3.3Animation from InteractionsLevel AAA · tested directly
  • 2.4.3Focus OrderLevel A · tested directly
  • 2.4.7Focus VisibleLevel AA · supporting test
  • 4.1.2Name, Role, ValueLevel A · tested directly

Fits any width.

Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.

0 px · drag the corner

Styling hooks

Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.

Usage
<Carousel className="shadow-sm" />

Used in blocks

Related components