Carousel
Primitive

Carousel

Carousel is a headless one-active-slide content rotator. It coordinates controlled or uncontrolled selection, optional automatic rotation, native touch scrolling, accessible controls, and inactive-slide semantics while the consumer or styled layer owns layout and appearance.

Live behavior
Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Use Previous, Next, the picker, or native horizontal scrolling to select one slide.

waiting for input

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

tsx
import { Carousel } from "@flowstack-ui/atom";

Anatomy

tsx
<Carousel.Root>
  <Carousel.RotationControl />
  <Carousel.Viewport>
    <Carousel.Track>
      <Carousel.Slide value="one" />
      <Carousel.Slide value="two" />
    </Carousel.Track>
  </Carousel.Viewport>
  <Carousel.Previous />
  <Carousel.Next />
  <Carousel.Picker>
    <Carousel.PickerItem value="one" />
    <Carousel.PickerItem value="two" />
  </Carousel.Picker>
</Carousel.Root>

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.

PropTypeDefault
valuestring-
defaultValuestring""
onValueChange(value, reason) => void-
autoPlayboolean-
defaultAutoPlaybooleanfalse
onAutoPlayChange(autoPlay) => void-
intervalnumber in milliseconds7000, minimum 1000
loopbooleantrue
dir"ltr" | "rtl"Direction context
previousAriaLabelstring"Previous slide"
nextAriaLabelstring"Next slide"
startAriaLabelstring"Start slide rotation"
stopAriaLabelstring"Stop slide rotation"
asChildbooleanfalse
renderRenderProp-

Root defaults to role="group"; consumers may pass role="region" only when the carousel warrants a landmark in the page information architecture.

ARIA attributeValues
aria-label"Featured content" by default; describe the content without repeating “carousel”
aria-roledescription"carousel" by default; may be localized
Data attributeValues
[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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-atomicfalse
aria-liveoff while automatic rotation is requested; otherwise polite
Data attributeValues
[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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[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.

PropTypeDefault
valuestringrequired
labelstring-
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
rolegroup by default
aria-labelauthored label or slide value
aria-roledescriptionslide by default; may be localized
aria-hiddentrue while inactive
Data attributeValues
[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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelRoot's previous label unless supplied directly
Data attributeValues
[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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelRoot's next label unless supplied directly
Data attributeValues
[data-slot]"carousel-next"
[data-direction]"next"
[data-disabled]present when unavailable

Picker

Picker renders a named group of optional direct-selection buttons.

PropTypeDefault
ariaLabelstring"Choose slide to display"
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
rolegroup
aria-labelariaLabel
Data attributeValues
[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.

PropTypeDefault
valuestringrequired
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelauthored label, registered Slide label, or Show <value>
aria-controlsassociated Slide ID
aria-disabledtrue for the active item
Data attributeValues
[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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelRoot's start or stop action label
Data attributeValues
[data-slot]"carousel-rotation-control"
[data-state]"playing" or "stopped"

Examples

Manually controlled content

tsx
import { Carousel } from "@flowstack-ui/atom";

export function ProductStories() {
  return (
    <Carousel.Root defaultValue="company" aria-label="Product stories">
      <Carousel.Viewport>
        <Carousel.Track>
          <Carousel.Slide value="company" label="Company services">
            Company services
          </Carousel.Slide>
          <Carousel.Slide value="hosting" label="Managed hosting">
            Managed hosting
          </Carousel.Slide>
        </Carousel.Track>
      </Carousel.Viewport>
      <Carousel.Previous />
      <Carousel.Next />
      <Carousel.Picker>
        <Carousel.PickerItem value="company" />
        <Carousel.PickerItem value="hosting" />
      </Carousel.Picker>
    </Carousel.Root>
  );
}

Automatic rotation

tsx
import { Carousel } from "@flowstack-ui/atom";

export function Announcements() {
  return (
    <Carousel.Root
      defaultValue="launch"
      defaultAutoPlay
      interval={8000}
      aria-label="Featured announcements"
    >
      <Carousel.RotationControl />
      <Carousel.Viewport>
        <Carousel.Track>
          <Carousel.Slide value="launch" label="New product launch">
            New product launch
          </Carousel.Slide>
          <Carousel.Slide value="service" label="Managed website service">
            Managed website service
          </Carousel.Slide>
        </Carousel.Track>
      </Carousel.Viewport>
      <Carousel.Previous />
      <Carousel.Next />
    </Carousel.Root>
  );
}

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.