When to Use
Use Carousel for a small sequence of interchangeable content that shares one
viewport, such as product campaigns, testimonials, announcements, or media
stories. Use Tabs when labeled controls switch document panels, SwipeableItem
when a row reveals actions, Pagination when navigating pages, and ordinary
layout or scrolling when all items should remain visible together.
Features
The behavior Atom owns before your product adds appearance.
- Controlled and uncontrolled active slide values.
- Optional controlled or uncontrolled automatic rotation.
- Previous, Next, direct picker, native touch, trackpad, and external
selection paths.
- Focus, hover, interaction, and document-visibility rotation policies.
- Direction-preserving looping or bounded collection navigation without cloned
slide content.
- LTR and RTL nearest-slide resolution.
- Group/carousel and group/slide semantics with localizable labels.
- Live-region policy plus inert, accessibility-hidden inactive slides.
asChild and render composition on every rendered part.
Import
Anatomy
Root, Viewport, Track, and one or more Slides form the required structure.
Every control is optional for a manually selected carousel. When automatic
rotation is enabled, RotationControl, Previous, and Next are required; Picker
remains optional.
API Reference
Root
Root renders a div, owns selection and rotation state, registers Slides in
DOM order, and supplies the carousel's accessible container semantics.
value | string | - |
defaultValue | string | "" |
onValueChange | (value, reason) => void | - |
autoPlay | boolean | - |
defaultAutoPlay | boolean | false |
onAutoPlayChange | (autoPlay) => void | - |
interval | number in milliseconds | 7000, minimum 1000 |
loop | boolean | true |
dir | "ltr" | "rtl" | Direction context |
previousAriaLabel | string | "Previous slide" |
nextAriaLabel | string | "Next slide" |
startAriaLabel | string | "Start slide rotation" |
stopAriaLabel | string | "Stop slide rotation" |
asChild | boolean | false |
render | RenderProp | - |
Root defaults to role="group"; consumers may pass role="region" only when
the carousel warrants a landmark in the page information architecture.
aria-label | "Featured content" by default; describe the content without repeating “carousel” |
aria-roledescription | "carousel" by default; may be localized |
[data-slot] | "carousel-root" |
[data-state] | "playing", "paused", or "stopped" |
[data-direction] | "ltr" or "rtl" |
[data-initialized] | present after the initial active Slide is aligned |
[data-loop-transition] | "next" or "previous" while crossing a loop boundary |
[data-value] | active slide value |
Root exposes --atom-carousel-count and --atom-carousel-index as headless
collection geometry for styled layers.
Styled layers should keep scroll motion instant until [data-initialized] is
present, then enable their ordinary motion recipe.
Viewport
Viewport renders a keyboard-focusable div, observes native scrolling, selects the nearest Slide,
and stops automatic rotation on pointer or wheel interaction. A styled layer
must make it the overflow viewport.
asChild | boolean | false |
render | RenderProp | - |
aria-atomic | false |
aria-live | off while automatic rotation is requested; otherwise polite |
[data-slot] | "carousel-viewport" |
[data-state] | "playing", "paused", or "stopped" |
[data-direction] | "ltr" or "rtl" |
Track
Track renders a div around Slides. It exposes direction but adds no layout;
the styled layer supplies its one-row track geometry. When loop has more
than one Slide, Track adds two empty, aria-hidden boundary spacers with
data-slot="carousel-loop-boundary". The spacers create scroll positions but
never clone authored slide content, IDs, controls, or form fields.
asChild | boolean | false |
render | RenderProp | - |
[data-slot] | "carousel-track" |
[data-direction] | "ltr" or "rtl" |
Slide
Slide renders a registered div. Values must be unique within Root. Provide a
short content label through label or aria-label; the fallback is value.
value | string | required |
label | string | - |
asChild | boolean | false |
render | RenderProp | - |
role | group by default |
aria-label | authored label or slide value |
aria-roledescription | slide by default; may be localized |
aria-hidden | true while inactive |
[data-slot] | "carousel-slide" |
[data-state] | "active" or "inactive" |
[data-value] | slide value |
[data-loop-position] | "before" or "after" when the styled layer must visually place the authored boundary Slide for a directional loop transition |
Inactive Slides are also inert so their descendants cannot remain in the focus
order while visually outside the viewport.
The styled layer must size each loop-boundary spacer to one viewport and move
the Slide carrying data-loop-position to that boundary. After native scroll
settles, Atom silently rebases the viewport to the authored Slide's ordinary
position. Next therefore continues forward from last to first and Previous
continues backward from first to last in both LTR and RTL.
Previous
Previous renders a native button and selects the prior Slide. With
loop={false}, it is disabled at the first Slide.
asChild | boolean | false |
render | RenderProp | - |
aria-label | Root's previous label unless supplied directly |
[data-slot] | "carousel-previous" |
[data-direction] | "previous" |
[data-disabled] | present when unavailable |
Next
Next renders a native button and selects the following Slide. With
loop={false}, it is disabled at the last Slide.
asChild | boolean | false |
render | RenderProp | - |
aria-label | Root's next label unless supplied directly |
[data-slot] | "carousel-next" |
[data-direction] | "next" |
[data-disabled] | present when unavailable |
Picker
Picker renders a named group of optional direct-selection buttons.
ariaLabel | string | "Choose slide to display" |
asChild | boolean | false |
render | RenderProp | - |
role | group |
aria-label | ariaLabel |
[data-slot] | "carousel-picker" |
[data-state] | "playing", "paused", or "stopped" |
PickerItem
PickerItem renders a native button associated with one Slide. The active item
uses aria-disabled rather than native disabled so it remains discoverable
in the tab sequence.
value | string | required |
asChild | boolean | false |
render | RenderProp | - |
aria-label | authored label, registered Slide label, or Show <value> |
aria-controls | associated Slide ID |
aria-disabled | true for the active item |
[data-slot] | "carousel-picker-item" |
[data-state] | "active" or "inactive" |
[data-value] | target slide value |
[data-disabled] | present when the target Slide is unavailable |
RotationControl
RotationControl renders a native button that explicitly starts or stops
automatic rotation. Its accessible label describes the action it will perform,
so it does not use aria-pressed.
asChild | boolean | false |
render | RenderProp | - |
aria-label | Root's start or stop action label |
[data-slot] | "carousel-rotation-control" |
[data-state] | "playing" or "stopped" |
Examples
Manually controlled content
Automatic rotation
Atom intentionally adds no layout CSS. The styled layer must make Viewport a
horizontal overflow container, Track a row, and each Slide one viewport wide
for touch scrolling and nearest-slide selection to operate as intended.
Accessibility
Carousel follows the
WAI-ARIA Carousel pattern
using grouped picker buttons rather than the optional tabbed variant.
Automatic rotation pauses while the pointer hovers the carousel or the
document is hidden. It stops when focus enters, a control is activated, or the
viewport receives pointer or wheel interaction, and does not restart until the
user activates RotationControl. Control activation does not move focus.
All controls use native button keyboard behavior; Atom does not intercept Tab
or Arrow keys for the Carousel itself. PickerItem adds one tab stop per Slide,
so use it for a small set and omit it when direct selection is unnecessary.
Do not place essential information only in an automatically rotating Slide.
Users must be able to stop rotation and reach every Slide without waiting.
Changelog
0.22.5
- Scoped programmatic alignment to the Carousel viewport so hydration and
late layout settlement cannot shift a full-width active slide partially out
of view or scroll an unrelated ancestor.
0.22.4
- Preserved the requested motion direction across loop boundaries by placing
the actual authored boundary Slide, settling native scrolling, and silently
rebasing without cloned content or duplicate interactive descendants; the
rebase also remains safe in DOM-like runtimes without
requestAnimationFrame.
0.22.3
- Replaced the focus/click timing workaround with explicit pointer-activation
provenance so RotationControl toggles exactly once across browser engines
while keyboard focus retains the required stop behavior.
0.22.1
- Fixed the focus-before-click race on RotationControl so activating a playing
carousel reliably stops it while ordinary focus entry retains the required
stop behavior.
0.22.0
- Added the one-active-slide Carousel family with controlled/uncontrolled
selection and rotation, optional controls and picker, native scroll-derived
touch selection, focus/hover/visibility pause policy, LTR/RTL support, and
WAI-ARIA carousel and slide semantics.