| allowMouseDrag | boolean | — | headless | Whether the Viewport supports mouse click-and-drag scrolling —
the pointer tracks 1:1 into scrollLeft/scrollTop (no momentum)
once past a small movement threshold, release lets the existing
scroll-snap-type settle. Defaults to false: an unconditionally-on
drag can conflict with a consumer's own drag-sensitive slide content
(a nested carousel, a draggable card, a canvas), so it's opt-in.
Touch/pen scrolling is unaffected either way — that's native,
independent of this prop. |
| ariaLabel | string | — | headless | Accessible name for the carousel region, announced as
aria-label. Prefer a short, human-readable description (e.g.
"Featured products"). Mutually exclusive with ariaLabelledBy. |
| ariaLabelledBy | string | — | headless | Id of an existing element to name the carousel region, set as
aria-labelledby. Use when a visible heading already labels the
carousel. Mutually exclusive with ariaLabel. |
| autoplay | CarouselAutoplay | — | headless | Autoplay configuration — see CarouselAutoplay. |
| defaultPage | number | — | headless | Uncontrolled active page index. Defaults to 0. |
| defaultPlaying | boolean | — | headless | Uncontrolled initial playing flag. Defaults to false. |
| ids | CarouselIds | — | headless | Pin DOM ids on the rendered sub-components — see
CarouselIds. Useful for SSR hydration stability and
external aria-controls linkage. |
| inViewThreshold | number | number[] | — | headless | Visibility threshold(s) the isInView fallback's
IntersectionObserver uses — both as the observer's own threshold
option and (for an array, the highest value) the cutoff a slide's
intersectionRatio must clear to count as "in view". Matches Ark
UI's inViewThreshold shape. Defaults to 0.6. |
| loop | boolean | "wrap" | "infinite" | — | headless | Wrap-around navigation mode — see CarouselLoopMode.
Defaults to false ("none"). Accepts a boolean for ergonomics or a
named mode:
- false / omitted → "none" (clamp at the ends).
- true / "wrap" → "wrap" (semantic wrap: Next/Previous and
autoplay wrap past the ends and the triggers never disable, but the
wrap last→first smooth-scrolls the track back — a visible rewind).
- "infinite" → continuous infinite: the same wrap page model plus a
cloned edge buffer and native-scroll recentre so the wrap is a
continuous glide with no rewind.
All wrapping requires more than one page — a single-page carousel has
no wrap target, so the triggers stay disabled regardless. The resolved
mode is published as data-loop="none" | "wrap" | "infinite" on the
Root. |
| onAutoplayStatusChange | (status: CarouselAutoplayStatus) => void | — | headless | Fires on every autoplay status transition — see
CarouselAutoplayStatus. Matches Ark UI's
onAutoplayStatusChange. Fires on every tick, not just play/pause
toggles — useful for analytics. No-op unless autoplay is enabled. |
| onDragStatusChange | (status: CarouselDragStatus) => void | — | headless | Fires on every mouse-drag status transition — see
CarouselDragStatus. Matches Ark UI's onDragStatusChange.
No-op unless allowMouseDrag is true (a drag can't start
otherwise). |
| onOverscrollStatusChange | (status: CarouselOverscrollStatus) => void | — | headless | Fires whenever the user pushes against a boundary with nowhere
further to go, from the keyboard, wheel, or a mouse drag — see
CarouselOverscrollStatus. |
| onPageChange | (page: number) => void | — | headless | Callback invoked when the active page should change (e.g. when the
user clicks Carousel.NextTrigger or Carousel.PreviousTrigger).
The callback is responsible for re-rendering with the new page. |
| onPlayingChange | (playing: boolean) => void | — | headless | Callback invoked when the playing flag should toggle. The callback
is responsible for re-rendering with the new playing value. |
| orientation | "horizontal" | "vertical" | — | headless | Axis the carousel scrolls and paginates along — see
CarouselOrientation. Defaults to "horizontal". Switches
the viewport scroll axis, the arrow-key bindings, and the
data-orientation styling hook on the Root. |
| page | number | — | headless | Controlled active page index. |
| playing | boolean | — | headless | Controlled playing flag. |
| slidesPerMove | number | "auto" | — | headless | Number of slides advanced by Carousel.NextTrigger /
Carousel.PreviousTrigger. "auto" (default) advances one full page
at a time (= slidesPerPage); a number advances exactly that many
slides per click and pages are windowed so the visible window always
stays full. A numeric move is coerced to an integer in
[1, slidesPerPage] — it can neither drop below one nor skip past a
page (which would orphan the slides in the gap) — and the last page
is end-aligned to the track end, so every slide stays reachable even
when the move doesn't divide the slide count evenly (e.g. 6 slides,
slidesPerPage={3}, slidesPerMove={2} yields pages
[0,1,2] [2,3,4] [3,4,5]). |
| slidesPerPage | number | — | headless | Number of slides visible per page. Defaults to 1. With values
greater than 1, slides are grouped into pages of that size for
navigation purposes: indicators auto-render per page, boundary
clamp moves to the last page, and Carousel.NextTrigger /
Carousel.PreviousTrigger advance one page at a time. The last
page always end-aligns (its window is full, flush with the track
end) rather than leaving a partial page — a partial page's leading
slide can't align to the viewport start, which desyncs the active
page against the scroll. So 7 slides at slidesPerPage={3} are 3
pages — [0,1,2] [3,4,5] [4,5,6] — the last shifted back by one to
stay full. Coerced to an integer ≥ 1 — 0, negative, fractional,
and non-finite values are clamped (e.g. 2.5 → 2, 0 → 1) so the
page maths never divides by zero. |
| snapAlign | "center" | "start" | "end" | — | headless | Scroll-snap alignment the Viewport targets when programmatically
scrolling to a page — see CarouselSnapAlign.
Defaults to "start". Set to "center" when consumer CSS uses
scroll-snap-align: center on slides (e.g. a peek layout
where slides are narrower than the Viewport). |
| snapType | "mandatory" | "proximity" | — | headless | scroll-snap-type strictness — see CarouselSnapType.
Defaults to "mandatory". |
| transition | "slide" | "fade" | "none" | — | headless | Visual transition mode — see CarouselTransition.
Defaults to "slide". |
| translations | CarouselTranslations | — | headless | Override the default user-visible strings the component owns —
see CarouselTranslations. Useful for i18n. |
| peek | "none" | "sm" | "md" | "lg" | none | styled | Reveal a sliver of the adjacent slides on either side of the active one. Works in both orientations (inline edges when horizontal, block edges when vertical). |
| gap | "none" | "sm" | "md" | "lg" | md | styled | The spacing between slides. A t-shirt scale re-pointing --primitiv-carousel-gap to a spacing token; md is the default. Works in both orientations (the gap runs on the scroll axis) and composes with every other variant. Note: the padding modifier couples the gap to its inset for a clean framed track, so it overrides this within a padded track. |
| padding | "none" | "sm" | "md" | "lg" | none | styled | Make the viewport a padded, framed track: pad the slides inward from the viewport edges and draw the track outline (border + rounded corners). The gap is coupled to the padding so the resting track doesn't itself reveal a neighbour; add peek for a deliberate reveal within the track. The background fill is opt-in via the surface modifier — padding alone renders an outlined track. |
| surface | "none" | "subtle" | none | styled | Opt into the viewport track's background fill. Off by default (a framed track is just an outline); pairs with padding to render a filled, framed track. |
| radius | "none" | "md" | none | styled | Opt into rounding the container (the viewport track around the slides), independent of the padding frame. Off by default (a square track); md rounds it to the shared, size/density-scaled carousel radius, so the track corners match the slide corners. (Distinct from the per-slide radius modifier, which rounds each slide; this rounds the track that clips them.) |
| placement | "external" | "overlay" | external | styled | Where the controls sit relative to the imagery — the one axis that is off-vs-on the slide. external (the default) keeps the controls off the imagery, in the space around the viewport. overlay insets them on the slide for edge-to-edge imagery. How the controls are arranged (one bar vs prev/next flanking the edges) is the orthogonal cluster axis; side / distribution / align / orientation then compose on top of both. |
| side | "after" | "before" | after | styled | Which cross-axis edge the indicator cluster (or the joined bar) sits on, relative to the scroll direction. after is the trailing edge (below the viewport when horizontal, the inline-end/right side when vertical); before is the leading edge (above / inline-start). Orientation-relative and RTL-safe — it composes with orientation to reach all four physical edges. Read by both placements and both clusters. |
| distribution | "group" | "stretch" | group | styled | How the controls spread along their edge. For joined it drives the whole prev/indicators/next bar — group (the default) bunches them, stretch pushes prev/next to the extremes with the indicators centred (space-between). For split it governs the indicator cluster (prev/next stay flanking the viewport edges) — group bunches the dots, stretch spreads them across the full edge. Applies to both placements. |
| align | "start" | "center" | "end" | center | styled | Where the grouped controls sit along their edge (only read under distribution=group; stretch fills the edge, so alignment is moot). center is the default; start / end pin the cluster to the leading / trailing end — the whole bar for joined, the indicator cluster for split. Applies to both placements. Logical, so it mirrors under RTL and follows the scroll axis when vertical. |
| cluster | "split" | "joined" | split | styled | How the controls are arranged — the orthogonal companion to placement, read by both external and overlay. split (the default) sends prev/next to the viewport's two scroll-axis edges (flanking it — left/right when horizontal, top/bottom when vertical) and leaves the indicators as a separate cluster positioned by side / distribution / align. joined bundles prev + indicators + next into one <CarouselControls> bar that travels together, positioned as a unit by side / distribution / align. Compose the parts inside <CarouselControls> for joined, as direct children of the root for split. Together with placement this is the full 2×2 — external/overlay × split/joined. |
| indicators | "dots" | "thumbnails" | dots | styled | What the indicators look like. dots (the default) is the compact dot row; thumbnails swaps each indicator for a rounded-rect image thumbnail — the active one ringed in the primary colour, the classic gallery pattern. Supply the thumbnail content as children of each <CarouselIndicator> (an <img> or a background element). |
| size | "xs" | "sm" | "md" | "lg" | "xl" | md | styled | Scale of the control chrome — the prev/next controls, indicator dots and thumbnails — on a t-shirt ramp, so a carousel matches the density of the UI around it. The viewport and slides stay container-driven (they fill their space); only the controls scale. Composes with the ambient data-density (which shifts every slot): size picks the slot, density shifts it. The prev/next controls track the shared framed-control ramp, so they match a same-size Button. |
| ratio | "square" | "standard" | "wide" | "ultrawide" | wide | styled | The carousel's aspect ratio, applied uniformly to every slide (and tracked by the thumbnail indicators). Re-points --primitiv-carousel-slide-aspect-ratio at the root; the vertical viewport's shape defaults to the same knob, so ratio shapes the slides in both orientations (square vertical slides, a widescreen vertical scroller, ...). Override the knob directly for a bespoke value. (Per-slide ratios are a later revisit.) |
| slideWidth | "equal" | "content" | equal | styled | How each slide's width (block size when vertical) is determined. equal (the default) shares the viewport's content box evenly across slidesPerPage slides, each holding its shape via the ratio aspect-ratio. content lets each slide size to its own content instead — an intrinsically-sized image, an explicit width on the slide, or any content with a natural size — so slides in one track can have genuinely different widths (Ark UI's autoSize). Scoped to slidesPerPage=1: the multi-slide windowing math assumes equal shares, which content-driven widths break. |
| effect | "none" | "parallax" | none | styled | An opt-in scroll-driven visual effect for the slide content, layered on top of scroll-snap paging. none (the default) renders slides plain. parallax gives each slide's <CarouselSlideContent> a native, zero-JavaScript drift as the slide crosses the viewport (Blossom Carousel's Slideshow example) — a CSS view-timeline scoped to the slide drives the content's transform via animation-range: cover, following the scroll axis (inline horizontal, block vertical). The effect requires wrapping the slide's media in <CarouselSlideContent>; browsers without animation-timeline: view() support fall back to reading the headless --slide-progress signal instead (an equivalent, continuously-updated transform with no extra JavaScript of our own), and it disables entirely under prefers-reduced-motion. |
| glide | "fast" | "medium" | "slow" | medium | styled | How fast the infinite loop glides between pages (loop="infinite" only — every other mode uses native scroll). A preset re-points --primitiv-carousel-glide-duration to a motion duration token; the engine reads it, with --primitiv-carousel-glide-easing, off the track and drives the transform transition. medium is the default. For a duration or easing outside the presets, re-point either custom property directly. |