Skip to content

Carousel

A flexible, CMS-first Carousel component powered by Embla internally. It accepts a normalized CarouselModel and renders either an interactive carousel or a static layout depending on the device mode and slide count. Supports drag, snap, loop, autoplay, controlled/uncontrolled index, custom arrows/dots, and full accessibility semantics, all without exposing the underlying engine.

import { Carousel } from "@/rokkit200-ui/components/carousel/carousel";
import type { CarouselModel } from "@/rokkit200-ui/components/carousel/carousel-types";
<Carousel
model={model}
renderSlide={(slide) => <SlideCard slide={slide} />}
/>

Setting loop: true in the model settings enables infinite looping. The carousel wraps from the last slide back to the first (and vice versa).

Enable autoplay with configurable delay through the autoplay settings. Autoplay automatically pauses on hover/focus and respects prefers-reduced-motion. stopOnMouseEnter and stopOnFocusIn set to true in demo to showcase behavior on hover and focus. stopOnInteraction set to false in order for autoplay to continue after.

Use the CarouselApi ref to programmatically control autoplay via the play and stop methods. The current state is exposed through autoplay.isPlaying() and kept in sync with the UI using autoplay events (autoplay:play and autoplay:stop). The carousel starts paused (playOnInit: false), and a shouldAutoplayRef (useRef(false)) stores the user’s intent so the autoplay state persists across reinitialization (e.g. screen resize). Additionally, stopOnMouseEnter, stopOnFocusIn, and stopOnInteraction are disabled so autoplay is controlled exclusively via the Play/Pause button.

Use activeIndex and onActiveIndexChange for fully controlled navigation. This makes the parent component the source of truth for the active page. External controls can update the state directly, while user interactions like dragging feed changes back through onActiveIndexChange, keeping everything synchronized.

In this demo, the custom buttons and numbered controls update the active page from outside the carousel, and the carousel reports user-driven changes back to the same state.

Page 1 / 3

Use a CarouselApi ref for imperative external control, together with onNavigationStateChange to mirror the carousel’s current state in surrounding UI.

This pattern works well when external controls such as custom arrows, dots, or page indicators need to drive the carousel directly, but the active page itself does not need to be owned as controlled prop state.

The demo below uses the ref to call goTo, goToPrev, and goToNext, while onNavigationStateChange keeps the external controls synchronized. It also shows how breakpoints, uiBreakpoints, and autoplay.breakpoints can adjust behavior and presentation across screen sizes.

External control — Initializing…

Use slidesVisible to show multiple slides at once. Pair it with slidesToScroll to control how far the carousel moves on each interaction, and use containScroll to keep edge alignment tidy when fewer snap positions are available near the end.

In this demo, the carousel shows one slide at a time on smaller screens. At wider breakpoints, it expands to three visible slides while navigation advances two slides per step, demonstrating that visible layout and scroll step can be configured independently.

Use renderLeftArrow, renderRightArrow, and renderDots to supply your own navigation UI instead of the default controls.

The arrow render props receive CarouselArrowCallbacks for previous and next navigation, including disabled-state helpers. The dots render prop receives CarouselDotsCallbacks, which expose the current active page, total page count, and a goTo method for direct navigation.

In this demo, the carousel keeps its built-in behavior, but the visible controls are fully customized.

When the model contains fewer than 2 slides, the carousel automatically renders a static layout with no interactive behavior, controls, or carousel ARIA semantics.

Setting deviceMode: "desktopOnly" makes the carousel interactive only on larger screens. On viewports below 1024px the slides render as a static layout. Use "mobileOnly" for the inverse behavior.

PropTypeDefaultDescription
modelCarouselModelNormalized carousel model: settings + ordered slides. Required.
renderSlide(slide: CarouselSlide, index: number) => ReactNodeRenders each slide’s content. Required.
classNamestringClass name for the root element.
viewportClassNamestringClass name for the viewport wrapper.
containerClassNamestringClass name for the container.
slideClassNamestringClass name for each slide wrapper.
slideStyleReact.CSSPropertiesInline styles merged onto each slide wrapper.
arrowsClassNamestringClass name for the arrows container.
arrowLeftClassNamestringClass name for the left arrow button.
arrowRightClassNamestringClass name for the right arrow button.
dotsClassNamestringClass name for the dots container.
dotsInnerClassNamestringClass name for the inner dots wrapper (default dots only).
activeIndexnumberControlled active slide index.
defaultActiveIndexnumber0Uncontrolled initial slide index.
onActiveIndexChange(index: number) => voidFired when active slide changes via internal interaction.
onNavigationStateChange(state: CarouselNavigationState) => voidFired on every navigation state update. Receives the full navigation snapshot.
renderLeftArrow(c: CarouselArrowCallbacks) => ReactNodeCustom left arrow renderer.
renderRightArrow(c: CarouselArrowCallbacks) => ReactNodeCustom right arrow renderer.
renderDots(c: CarouselDotsCallbacks) => ReactNodeCustom dots renderer.
ariaLabelstringOverride carousel accessible name.
ariaLabelledBystringOverride via aria-labelledby.
suppressWarningsbooleanSilence all dev warnings.
FieldTypeDescription
titlestring?Optional title.
settingsCarouselSettingsCarousel behavior configuration.
slidesCarouselSlide[]Ordered slide data.
FieldTypeDescription
idstringUnique identifier for the slide.
contentunknownArbitrary slide payload — interpreted by renderSlide.
labelstring?Custom aria-label for the slide. Falls back to “Slide n of total”.
FieldTypeDefaultDescription
deviceMode"all" | "desktopOnly" | "mobileOnly""all"Determines when carousel is interactive.
showDotsbooleantrueShow dot navigation.
showArrowsbooleantrueShow prev/next arrows.
slidesVisiblenumber?1Number of slides visible at once. Dots reflect page count.
loopbooleanfalseEnable infinite loop.
watchDragbooleantrueEnable drag interaction.
slidesToScrollnumber | "auto"1Slides advanced per interaction. "auto" scrolls by slidesVisible.
containScroll"trimSnaps" | "keepSnaps" | false"trimSnaps"Contain scroll behavior at edges. false disables containment.
breakpointsCarouselBreakpoints?Media-query keyed overrides for Embla scroll settings (loop, slidesToScroll, etc.).
uiBreakpointsCarouselUiBreakpoints?Media-query keyed overrides for UI visibility (showArrows, showDots, slidesVisible).
autoplayCarouselAutoplaySettings?Autoplay configuration. Disabled when prefers-reduced-motion is active.
carouselAriaLabelstring?"Carousel"Default accessible name for the carousel region.
prevButtonLabelstring?"Previous slide"Previous arrow aria-label.
nextButtonLabelstring?"Next slide"Next arrow aria-label.
FieldTypeDefaultDescription
activeboolean?trueEnable autoplay.
delaynumber?4000Milliseconds between slides.
stopOnMouseEnterboolean?falsePause when the pointer enters the carousel.
stopOnFocusInboolean?falsePause when a child element receives focus.
stopOnInteractionboolean?trueStop permanently after user interacts.
playOnInitboolean?trueBegin playing immediately on mount.
breakpointsCarouselAutoplayBreakpoints?Media-query keyed overrides for autoplay behavior.
FieldTypeDescription
activeIndexnumberCurrent active slide/page index.
slideCountnumberTotal number of slides.
pageCountnumberTotal snap pages (depends on grouping).
canGoToPrevbooleanWhether backward navigation is possible.
canGoToNextbooleanWhether forward navigation is possible.
FieldDescription
selectFired when the selected slide/page changes.
reInitFired when the carousel reinitializes, such as after resize.
autoplay:playFired when autoplay starts or resumes.
autoplay:stopFired when autoplay stops or is paused.
MethodTypeDescription
getActiveIndex() => numberGet current active slide index.
getSlideCount() => numberGet total slide count.
getPageCount() => numberGet total snap page count.
goTo(index, opts?) => voidNavigate to a specific slide.
goToNext(opts?) => voidNavigate to next slide.
goToPrev(opts?) => voidNavigate to previous slide.
canGoToNext() => booleanWhether forward navigation is possible.
canGoToPrev() => booleanWhether backward navigation is possible.
autoplayCarouselAutoplayApi?Autoplay control object. undefined when autoplay is inactive.
MethodTypeDescription
play(opts?) => voidResume autoplay.
stop() => voidStop autoplay.
reset() => voidReset the autoplay timer.
isPlaying() => booleanWhether autoplay is currently running.
timeUntilNext() => number | nullMilliseconds until the next slide, or null.

The Carousel renders with full ARIA semantics when interactive:

  • The root element has role="region" with aria-roledescription="carousel" and an accessible name via aria-label or aria-labelledby.
  • Each slide has role="group" with aria-roledescription="slide" and an aria-label that defaults to “Slide n of total” (overridable via CarouselSlide.label).
  • Previous/next arrow buttons have configurable aria-label via prevButtonLabel and nextButtonLabel in settings.
  • Default dot navigation uses role="tablist" with individual role="tab" and aria-selected for the active page.
  • Autoplay automatically pauses when prefers-reduced-motion: reduce is active.