Carousel Controls
- Stable
- WCAG 2.2 evidence
- RTL
Previous / Next buttons for a carousel, and optionally a button that stops and starts automatic rotation. There is no carousel engine: you render the slides and show the one at index; these controls
only move the index and the rotation state. Built on React Aria Buttons in a group: Enter and Space press them, keyboard focus shows the focus ring, and every
button is a 40 px target (WCAG 2.1.1, 2.4.7, 2.5.8, 4.1.2). Without isLooping the ends are aria-disabled rather than
disabled, so focus stays on the button that reached the end. The chevrons mirror in right-to-left layouts (Previous
points right); the play / pause glyphs do not. The rotation button changes its name and icon ("Stop automatic slide
show" / "Start automatic slide show") instead of using aria-pressed. Consumer duties (APG carousel pattern): put the controls before the slides in a region or group with
aria-roledescription="carousel" and a name; label each slide ("3 of 5"); make the slide container aria-live="polite"
while not rotating ("off" while rotating); stop rotation while keyboard focus or the pointer is in the carousel and
do not start it when the user prefers reduced motion (2.2.2); translate the labels.
import { CarouselControls } from "@nexera-ui/react";- 5
- examples
- 15
- props
- 5
- live controls
- 1
- platform
- 9
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { CarouselControls } from "@nexera-ui/react";
<CarouselControls />Examples 4
The same examples as Storybook, rendered live. Open Code to copy one.
Arrows and play
Type=Arrows and play: the rotation button stops and starts automatic rotation (WCAG 2.2.2).
import { CarouselControls } from "@nexera-ui/react";
<CarouselControls count={5} defaultIndex={1} aria-label="Team photos" type="arrows-and-play" />;
On media
Surface=On media: white buttons with a shadow, over a picture.
import { CarouselControls } from "@nexera-ui/react";
const PHOTO = `data:image/svg+xml,${encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 225"><rect width="400" height="225" fill="lightsteelblue"/>' +
'<path d="M0 170 L120 100 L210 160 L290 120 L400 170 L400 225 L0 225 Z" fill="seagreen"/></svg>',
)}`;
export function OnMedia() {
return (
<div className="relative w-full max-w-md overflow-hidden rounded-lg">
<img src={PHOTO} alt="" className="block aspect-video w-full object-cover" />
<CarouselControls
count={5}
defaultIndex={1}
aria-label="Team photos"
type="arrows-and-play"
surface="on-media"
className="absolute bottom-3 end-3"
/>
</div>
);
}
Looping
isLooping: Next on the last slide goes back to the first, so no end is disabled.
import { CarouselControls } from "@nexera-ui/react";
<CarouselControls count={5} defaultIndex={4} aria-label="Team photos" isLooping />;
Carousel recipe
Recipe (no carousel engine ships): the APG carousel pattern composed from CarouselControls, CarouselIndicator and your slides. The region is named and described as a carousel, the controls come first, the slide container is aria-live="polite" while not rotating, rotation stops while the pointer or keyboard focus is inside, and it never starts when the user prefers reduced motion.
import { useEffect, useId, useState } from "react";
import { useFocusWithin, useHover } from "react-aria";
import { Carousel, CarouselControls, CarouselIndicator } from "@nexera-ui/react";
export function CarouselRecipe() {
const slides = [
{ title: "Welcome week", text: "Meet your buddy on day two.", tone: "bg-status-info-bg" },
{
title: "Benefits",
text: "Enrol before the end of the month.",
tone: "bg-status-success-bg",
},
{
title: "Leave",
text: "Request time off from the Leave page.",
tone: "bg-status-warning-bg",
},
{ title: "Payroll", text: "Payslips arrive on the 25th.", tone: "bg-status-leave-bg" },
];
function Carousel() {
const slidesId = useId();
const [index, setIndex] = useState(0);
const [isPlaying, setPlaying] = useState(
() => !window.matchMedia("(prefers-reduced-motion: reduce)").matches,
);
// Rotation stops while the pointer or keyboard focus is in the carousel.
const { hoverProps, isHovered } = useHover({});
const [isFocusWithin, setFocusWithin] = useState(false);
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setFocusWithin });
const rotating = isPlaying && !isHovered && !isFocusWithin;
useEffect(() => {
if (!rotating) return undefined;
const timer = window.setInterval(() => {
setIndex((current) => (current + 1) % slides.length);
}, 5000);
return () => {
window.clearInterval(timer);
};
}, [rotating]);
return (
<section
aria-roledescription="carousel"
aria-label="Onboarding highlights"
className="flex w-full max-w-xl flex-col gap-3"
{...mergeProps(hoverProps, focusWithinProps)}
>
<div className="flex flex-wrap items-center justify-between gap-3">
<CarouselControls
type="arrows-and-play"
count={slides.length}
index={index}
onIndexChange={setIndex}
isLooping
isPlaying={isPlaying}
onPlayingChange={setPlaying}
slidesId={slidesId}
/>
<CarouselIndicator
count={slides.length}
index={index}
onIndexChange={setIndex}
slidesId={slidesId}
aria-label="Choose a slide"
/>
</div>
<div id={slidesId} aria-live={rotating ? "off" : "polite"}>
{slides.map((slide, slideIndex) => (
<div
key={slide.title}
role="group"
aria-roledescription="slide"
aria-label={`${String(slideIndex + 1)} of ${String(slides.length)}`}
hidden={slideIndex !== index}
className={`rounded-lg p-6 ${slide.tone}`}
>
<h3 className="m-0 text-heading-h4 text-primary">{slide.title}</h3>
<p className="m-0 mt-1 text-body-default text-secondary">{slide.text}</p>
</div>
))}
</div>
</section>
);
}
return <Carousel />;
}
Props 15
Press "Try it" on a card to load that prop into the playground.
15 props shown
typeNexera"arrows" | "arrows-and-play"Buttons shown: arrows (Previous, Next) or arrows-and-play (plus a button that stops and starts
automatic rotation; required whenever slides rotate on their own, WCAG 2.2.2).
surfaceNexera"on-light" | "on-media"Background the controls sit on: on-light (outlined surface buttons) or on-media (white buttons
with a shadow, over a picture).
count*NexeranumberNumber of slides. With isLooping off, Previous is unavailable on the first slide and Next on the last.
indexNexeranumberIndex of the current slide, from 0 (controlled). Pair it with onIndexChange.
defaultIndexNexeranumberIndex of the current slide at first render (uncontrolled).
onIndexChangeNexera(index: number) => voidCalled with the new slide index when Previous or Next is pressed. Show that slide.
isLoopingNexerabooleanWrap around: Next on the last slide goes to the first, Previous on the first goes to the last. Without it the ends
are aria-disabled (the button keeps focus, presses do nothing).
isPlayingNexerabooleanWhether the slides rotate automatically (controlled), shown by the rotation button: Figma Pause while rotating, a
play icon otherwise. Pair it with onPlayingChange; the library has no timer, you run the rotation.
defaultPlayingNexerabooleanRotation state at first render (uncontrolled). The Figma Arrows and play variant shows Pause, so the default is
rotating; pass false when the user prefers reduced motion.
onPlayingChangeNexera(isPlaying: boolean) => voidCalled with the new rotation state when the rotation button is pressed. Stop or start your timer.
slidesIdNexerastringId of the element that holds the slides; set as aria-controls on the buttons.
isDisabledNexerabooleanDisables every button (skipped by Tab).
labelsNexeraPartial<CarouselControlsLabels>Accessible names of the buttons; English defaults. Translate them.
classNameNexerastringExtra classes for the root, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style for the root.
* 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
6 direct · 3 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.4.11Non-text ContrastLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.2.2Pause, Stop, HideLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · tested directly
- 2.5.8Target Size (Minimum)Level 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.
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:.
data-disableddata-hovereddata-pressed
<CarouselControls className="data-disabled:opacity-90 shadow-sm" />Used in blocks
Related components
- CarouselA carousel: slides in a scroll-snap strip with Previous / Next, indicators and, when you opt in, automatic rotation with a visible pause button.
- CarouselIndicatorShows which slide of a carousel is current, and lets people jump to a slide.
- ImageA picture in a rounded frame with a fixed aspect ratio: Figma `State=Loading` (a `bg/sunken` frame with a shimmer), `Loaded` and `Error` ("Image unavailable") follow the load lifecycle of the `<img>`; they are not props.
- ImageCropperShows an image with a crop frame people drag to move and resize, free or locked to an aspect ratio, in a rectangle or a circle, with a live CSS filter preview.
- ImageTileA square picture in a gallery or picker, with overlay actions on hover and a selected state.
- VideoControlsThe control bar of a video: scrubber, play / pause, back, mute and volume, time, captions, settings, picture in picture and full screen, over a dark gradient.
- VideoPlayerA video with a poster, Nexera controls and captions.