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, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const slides = [ { id: "default-s1", content: { title: "Slide 1" }, }, { id: "default-s2", content: { title: "Slide 2" }, }, { id: "default-s3", content: { title: "Slide 3" }, },];
const model: CarouselModel = { title: "Default", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, loop: false, watchDrag: true, carouselAriaLabel: "Carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselDefaultDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} /> );}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} />}/>Examples
Section titled “Examples”Setting loop: true in the model settings enables infinite looping. The carousel wraps from the last slide back to the first (and vice versa).
import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const slides = [ { id: "loop-s1", content: { title: "Slide 1" }, }, { id: "loop-s2", content: { title: "Slide 2" }, }, { id: "loop-s3", content: { title: "Slide 3" }, }, { id: "loop-s4", content: { title: "Slide 4" }, },];
const model: CarouselModel = { title: "Loop", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 2, slidesToScroll: 2, containScroll: "keepSnaps", loop: true, watchDrag: true, carouselAriaLabel: "Looping Carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselLoopDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} /> );}Autoplay
Section titled “Autoplay”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.
import { Button } from "@/registry/rokkit200-ui/components/button/button";import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const slides = [ { id: "autoplay-s1", content: { title: "Slide 1" }, }, { id: "autoplay-s2", content: { title: "Slide 2" }, }, { id: "autoplay-s3", content: { title: "Slide 3" }, },];
const model: CarouselModel = { title: "Autoplay", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, loop: true, watchDrag: true, autoplay: { active: true, delay: 4000, stopOnMouseEnter: true, stopOnFocusIn: true, stopOnInteraction: false, }, carouselAriaLabel: "Auto-playing Carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string }; return ( <div className={`flex h-48 flex-col items-center justify-center gap-4 rounded-xl bg-zinc-100 text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> <span className="text-2xl font-semibold">{title}</span> <span className="text-sm opacity-80"> Hover to pause · Focus a button to pause </span> <Button variant="secondary" type="button" onClick={e => e.preventDefault()}> Focusable button </Button> </div> );}
export default function CarouselAutoplayDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} /> );}Autoplay with Pause/Play Control
Section titled “Autoplay with Pause/Play Control”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.
import { Button } from "@/registry/rokkit200-ui/components/button/button";import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselApi, CarouselSlide,} from "@/registry/rokkit200-ui/components/carousel/carousel-types";import { useCallback, useEffect, useRef, useState } from "react";
const slides: CarouselSlide[] = [ { id: "autoplay-control-s1", content: { title: "Slide 1" }, }, { id: "autoplay-control-s2", content: { title: "Slide 2" }, }, { id: "autoplay-control-s3", content: { title: "Slide 3" }, },];
const model: CarouselModel = { title: "Controlled Autoplay", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, loop: true, watchDrag: true, autoplay: { active: true, delay: 4000, stopOnMouseEnter: false, stopOnFocusIn: false, stopOnInteraction: true, playOnInit: false, }, carouselAriaLabel: "Pausable Carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className="flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100"> {title} </div> );}
export default function CarouselAutoplayControlDemo() { const carouselRef = useRef<CarouselApi | null>(null); const [isPlaying, setIsPlaying] = useState(false); const [isReady, setIsReady] = useState(false);
const onAutoplayPlay = useCallback(() => setIsPlaying(true), []); const onAutoplayStop = useCallback(() => setIsPlaying(false), []);
useEffect(() => { const api = carouselRef.current; if (!api) return;
// First pass: embla may not be initialized yet; trigger a re-run once it is. if (!isReady) { setIsReady(true); return; }
// Sync initial UI state with the actual autoplay state. setIsPlaying(api.autoplay?.isPlaying() ?? false);
api.on("autoplay:play", onAutoplayPlay); api.on("autoplay:stop", onAutoplayStop);
return () => { api.off("autoplay:play", onAutoplayPlay); api.off("autoplay:stop", onAutoplayStop); }; }, [isReady, onAutoplayPlay, onAutoplayStop]);
const toggleAutoplay = useCallback(() => { const autoplay = carouselRef.current?.autoplay; if (!autoplay) return;
if (autoplay.isPlaying()) { autoplay.stop(); } else { autoplay.play(); } }, []);
return ( <div className="flex w-full flex-col gap-4"> <div className="flex min-h-10 items-center gap-4"> {isReady ? ( <> <Button variant="secondary" type="button" onClick={toggleAutoplay} aria-label={isPlaying ? "Stop autoplay" : "Start autoplay"}> {isPlaying ? ( <> <StopIcon /> Stop </> ) : ( <> <PlayIcon /> Start </> )} </Button>
<span className="text-sm text-slate-500 dark:text-slate-400"> {isPlaying ? "Auto-advancing every 4s" : "Stopped"} </span> </> ) : null} </div>
<Carousel ref={carouselRef} model={model} renderSlide={slide => <SlideCard slide={slide} />} /> </div> );}
function StopIcon() { return ( <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <rect x="4" y="4" width="16" height="16" rx="2" /> </svg> );}
function PlayIcon() { return ( <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <path d="M8 5v14l11-7z" /> </svg> );}Controlled Via Props
Section titled “Controlled Via Props”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.
import { cn } from "@/lib/utils";import { Button } from "@/registry/rokkit200-ui/components/button/button";import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";import { useState } from "react";
const SLIDES_VISIBLE = 3;
const slides = Array.from({ length: 9 }, (_, i) => ({ id: `controlled-props-s${i + 1}`, content: { title: `Slide ${i + 1}`, },}));
const PAGE_COUNT = Math.ceil(slides.length / SLIDES_VISIBLE);
const model: CarouselModel = { title: "Controlled Props", settings: { deviceMode: "all", showDots: false, showArrows: false, watchDrag: true, slidesVisible: 1, slidesToScroll: 1, loop: false, breakpoints: { "(min-width: 768px)": { slidesToScroll: "auto", }, },
uiBreakpoints: { "(min-width: 768px)": { slidesVisible: SLIDES_VISIBLE, }, }, carouselAriaLabel: "Props-controlled carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-40 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselControlledPropsDemo() { const [activePage, setActivePage] = useState(0);
return ( <div className="flex w-full flex-col gap-4"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center"> <span className="text-sm font-medium text-slate-700 dark:text-slate-300"> Page {activePage + 1} / {PAGE_COUNT} </span> <div className="flex gap-2"> <Button variant="secondary" onClick={() => setActivePage(p => Math.max(0, p - 1))} disabled={activePage === 0} className="max-w-max"> Prev </Button> <Button variant="secondary" onClick={() => setActivePage(p => Math.min(PAGE_COUNT - 1, p + 1))} disabled={activePage === PAGE_COUNT - 1} className="max-w-max"> Next </Button> </div> <div className="flex gap-1"> {Array.from({ length: PAGE_COUNT }, (_, i) => ( <Button key={i} kind="icon" role="tab" aria-selected={i === activePage} aria-label={`Go to page ${i + 1}`} onClick={() => setActivePage(i)} variant={i === activePage ? "primary" : "secondary"} className={cn( "h-9 w-9 p-0 text-sm font-semibold transition-colors", i === activePage && "hover:bg-slate-500 dark:hover:bg-slate-400" )}> {i + 1} </Button> ))} </div> </div>
<Carousel model={model} activeIndex={activePage} onActiveIndexChange={setActivePage} renderSlide={slide => <SlideCard slide={slide} />} /> </div> );}Controlled Via Ref
Section titled “Controlled Via Ref”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.
"use client";
import { cn } from "@/lib/utils";import { Button } from "@/registry/rokkit200-ui/components/button/button";import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselApi, CarouselNavigationState, CarouselSlide,} from "@/registry/rokkit200-ui/components/carousel/carousel-types";import * as React from "react";
const SLIDES_VISIBLE = 3;
const slides = Array.from({ length: 9 }, (_, i) => ({ id: `controlled-ref-s${i + 1}`, content: { title: `Slide ${i + 1}`, },}));
const model: CarouselModel = { title: "Controlled Ref", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, slidesToScroll: 1, containScroll: "trimSnaps", loop: false, watchDrag: true, autoplay: { active: true, delay: 4000, stopOnMouseEnter: true, stopOnFocusIn: true, stopOnInteraction: true, breakpoints: { "(min-width: 768px)": { active: false, }, }, },
breakpoints: { "(min-width: 768px)": { slidesToScroll: "auto", }, },
uiBreakpoints: { "(min-width: 768px)": { showArrows: false, showDots: false, slidesVisible: SLIDES_VISIBLE, }, },
carouselAriaLabel: "Controlled Carousel Via Ref", }, slides,};
function ChevronIcon() { return ( <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="m15 18-6-6 6-6" /> </svg> );}
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselControlledRefDemo() { const carouselRef = React.useRef<CarouselApi>(null); const [navState, setNavState] = React.useState({ activeIndex: 0, slideCount: slides.length, pageCount: 0, canGoToPrev: false, canGoToNext: false, }); const [navReady, setNavReady] = React.useState(false);
const handleNavigationStateChange = React.useCallback( (state: CarouselNavigationState) => { setNavState(state); setNavReady(true); }, [] );
const handleGoTo = React.useCallback((index: number) => { carouselRef.current?.goTo(index); }, []);
const handlePrev = React.useCallback(() => { carouselRef.current?.goToPrev(); }, []);
const handleNext = React.useCallback(() => { carouselRef.current?.goToNext(); }, []);
return ( <div className="flex w-full flex-col gap-4"> <div className="flex flex-wrap items-center gap-4"> <span className="text-sm text-slate-500 dark:text-slate-400"> {navReady ? `External control — Current: ${navState.activeIndex + 1} / ${navState.pageCount}` : "External control — Initializing…"} </span>
{navReady && navState.pageCount > 0 && ( <div className="flex gap-1"> {Array.from({ length: navState.pageCount }, (_, i) => ( <Button key={i} kind="icon" onClick={() => handleGoTo(i)} variant={i === navState.activeIndex ? "primary" : "secondary"} className={cn( "h-9 w-9 p-0 text-sm font-semibold transition-colors", i === navState.activeIndex && "hover:bg-slate-500 dark:hover:bg-slate-400" )} aria-label={`Go to page ${i + 1}`}> {i + 1} </Button> ))} </div> )} </div>
<Carousel ref={carouselRef} model={model} onNavigationStateChange={handleNavigationStateChange} renderSlide={slide => <SlideCard slide={slide} />} />
{navReady && ( <div className="hidden items-center justify-between gap-2 md:flex"> <div className="flex items-center justify-center gap-2 py-2" role="tablist" aria-label="Slide navigation"> {Array.from({ length: navState.pageCount }, (_, i) => ( <Button key={i} kind="icon" onClick={() => handleGoTo(i)} variant={i === navState.activeIndex ? "primary" : "secondary"} className={cn( "h-2.5 w-2.5 rounded-full p-0 transition-colors", i === navState.activeIndex && "hover:bg-slate-500 dark:hover:bg-slate-400" )} aria-label={`Go to page ${i + 1}`} /> ))} </div>
<div className="ml-4 flex gap-2"> <Button variant="secondary" onClick={handlePrev} disabled={!navState.canGoToPrev} aria-label="Previous slide"> <ChevronIcon /> </Button>
<Button variant="secondary" className="[&_svg]:rotate-180" onClick={handleNext} disabled={!navState.canGoToNext} aria-label="Next slide"> <ChevronIcon /> </Button> </div> </div> )} </div> );}Slides Per View
Section titled “Slides Per View”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.
import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const SLIDES_VISIBLE = 3;
const slides = Array.from({ length: 9 }, (_, i) => ({ id: `s${i + 1}`, content: { title: `Slide ${i + 1}`, },}));
const model: CarouselModel = { title: "Slides Per View",
settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, slidesToScroll: 1, containScroll: "trimSnaps", loop: false, watchDrag: true,
breakpoints: { "(min-width: 768px)": { slidesToScroll: 2, }, }, uiBreakpoints: { "(min-width: 768px)": { slidesVisible: SLIDES_VISIBLE, }, },
carouselAriaLabel: "Multi Slide Per View Carousel", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-40 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselSlidesPerViewDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} /> );}Custom Controls
Section titled “Custom Controls”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.
import { cn } from "@/lib/utils";import { Button } from "@/registry/rokkit200-ui/components/button/button";import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselArrowCallbacks, CarouselDotsCallbacks, CarouselSlide,} from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const slides = [ { id: "custom-control-s1", content: { title: "Slide 1" }, }, { id: "custom-control-s2", content: { title: "Slide 2" }, }, { id: "custom-control-s3", content: { title: "Slide 3" }, },];
const model: CarouselModel = { title: "Custom Controls", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, loop: false, watchDrag: true, carouselAriaLabel: "Custom Controls Carousel", }, slides,};
function CustomArrow({ direction, onClick, disabled,}: { direction: "left" | "right"; onClick: () => void; disabled: boolean;}) { return ( <Button type="button" variant="outline" onClick={onClick} disabled={disabled} aria-label={direction === "left" ? "Previous" : "Next"}> {direction === "left" ? "Prev" : "Next"} </Button> );}
function CustomDots(c: CarouselDotsCallbacks) { const activeIndex = c.getActiveIndex(); const pageCount = c.getPageCount();
return ( <div className="flex items-center justify-center gap-2 py-2" role="tablist" aria-label="Slide navigation"> {Array.from({ length: pageCount }, (_, i) => { const isActive = i === activeIndex;
return ( <Button key={i} kind="icon" role="tab" aria-selected={isActive} aria-label={`Go to page ${i + 1}`} onClick={() => c.goTo(i)} variant={isActive ? "primary" : "secondary"} className={cn( "h-9 w-9 p-0 text-sm font-semibold transition-colors", isActive && "hover:bg-slate-500 dark:hover:bg-slate-400" )}> {i + 1} </Button> ); })} </div> );}
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselCustomControlsDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} renderLeftArrow={(c: CarouselArrowCallbacks) => ( <CustomArrow direction="left" onClick={() => c.goToPrev()} disabled={!c.canGoToPrev()} /> )} renderRightArrow={(c: CarouselArrowCallbacks) => ( <CustomArrow direction="right" onClick={() => c.goToNext()} disabled={!c.canGoToNext()} /> )} renderDots={(c: CarouselDotsCallbacks) => <CustomDots {...c} />} /> );}Static Fallback
Section titled “Static Fallback”When the model contains fewer than 2 slides, the carousel automatically renders a static layout with no interactive behavior, controls, or carousel ARIA semantics.
import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const model: CarouselModel = { title: "Static Fallback", settings: { deviceMode: "all", showDots: true, showArrows: true, slidesVisible: 1, loop: false, watchDrag: true, carouselAriaLabel: "Single slide example", }, slides: [ { id: "only-slide", content: { title: "Only Slide" }, }, ],};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselStaticFallbackDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} /> );}Desktop Only
Section titled “Desktop Only”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.
import { Carousel, type CarouselModel,} from "@/registry/rokkit200-ui/components/carousel/carousel";import type { CarouselSlide } from "@/registry/rokkit200-ui/components/carousel/carousel-types";
const slides = [ { id: "desktop-only-s1", content: { title: "Slide 1" }, }, { id: "desktop-only-s2", content: { title: "Slide 2" }, }, { id: "desktop-only-s3", content: { title: "Slide 3" }, },];
const model: CarouselModel = { title: "Desktop Only", settings: { deviceMode: "desktopOnly", showDots: true, showArrows: true, slidesToScroll: 1, loop: false, watchDrag: true, carouselAriaLabel: "Single slide example", }, slides,};
function SlideCard({ slide }: { slide: CarouselSlide }) { const { title } = slide.content as { title: string };
return ( <div className={`flex h-48 items-center justify-center rounded-xl bg-zinc-100 text-2xl font-semibold text-slate-900 dark:bg-zinc-800 dark:text-slate-100`}> {title} </div> );}
export default function CarouselDesktopOnlyDemo() { return ( <Carousel model={model} renderSlide={slide => <SlideCard slide={slide} />} enableDataAttributes={true} /> );}API Reference
Section titled “API Reference”CarouselProps
Section titled “CarouselProps”| Prop | Type | Default | Description |
|---|---|---|---|
model | CarouselModel | — | Normalized carousel model: settings + ordered slides. Required. |
renderSlide | (slide: CarouselSlide, index: number) => ReactNode | — | Renders each slide’s content. Required. |
className | string | — | Class name for the root element. |
viewportClassName | string | — | Class name for the viewport wrapper. |
containerClassName | string | — | Class name for the container. |
slideClassName | string | — | Class name for each slide wrapper. |
slideStyle | React.CSSProperties | — | Inline styles merged onto each slide wrapper. |
arrowsClassName | string | — | Class name for the arrows container. |
arrowLeftClassName | string | — | Class name for the left arrow button. |
arrowRightClassName | string | — | Class name for the right arrow button. |
dotsClassName | string | — | Class name for the dots container. |
dotsInnerClassName | string | — | Class name for the inner dots wrapper (default dots only). |
activeIndex | number | — | Controlled active slide index. |
defaultActiveIndex | number | 0 | Uncontrolled initial slide index. |
onActiveIndexChange | (index: number) => void | — | Fired when active slide changes via internal interaction. |
onNavigationStateChange | (state: CarouselNavigationState) => void | — | Fired on every navigation state update. Receives the full navigation snapshot. |
renderLeftArrow | (c: CarouselArrowCallbacks) => ReactNode | — | Custom left arrow renderer. |
renderRightArrow | (c: CarouselArrowCallbacks) => ReactNode | — | Custom right arrow renderer. |
renderDots | (c: CarouselDotsCallbacks) => ReactNode | — | Custom dots renderer. |
ariaLabel | string | — | Override carousel accessible name. |
ariaLabelledBy | string | — | Override via aria-labelledby. |
suppressWarnings | boolean | — | Silence all dev warnings. |
CarouselModel
Section titled “CarouselModel”| Field | Type | Description |
|---|---|---|
title | string? | Optional title. |
settings | CarouselSettings | Carousel behavior configuration. |
slides | CarouselSlide[] | Ordered slide data. |
CarouselSlide
Section titled “CarouselSlide”| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the slide. |
content | unknown | Arbitrary slide payload — interpreted by renderSlide. |
label | string? | Custom aria-label for the slide. Falls back to “Slide n of total”. |
CarouselSettings
Section titled “CarouselSettings”| Field | Type | Default | Description |
|---|---|---|---|
deviceMode | "all" | "desktopOnly" | "mobileOnly" | "all" | Determines when carousel is interactive. |
showDots | boolean | true | Show dot navigation. |
showArrows | boolean | true | Show prev/next arrows. |
slidesVisible | number? | 1 | Number of slides visible at once. Dots reflect page count. |
loop | boolean | false | Enable infinite loop. |
watchDrag | boolean | true | Enable drag interaction. |
slidesToScroll | number | "auto" | 1 | Slides advanced per interaction. "auto" scrolls by slidesVisible. |
containScroll | "trimSnaps" | "keepSnaps" | false | "trimSnaps" | Contain scroll behavior at edges. false disables containment. |
breakpoints | CarouselBreakpoints? | — | Media-query keyed overrides for Embla scroll settings (loop, slidesToScroll, etc.). |
uiBreakpoints | CarouselUiBreakpoints? | — | Media-query keyed overrides for UI visibility (showArrows, showDots, slidesVisible). |
autoplay | CarouselAutoplaySettings? | — | Autoplay configuration. Disabled when prefers-reduced-motion is active. |
carouselAriaLabel | string? | "Carousel" | Default accessible name for the carousel region. |
prevButtonLabel | string? | "Previous slide" | Previous arrow aria-label. |
nextButtonLabel | string? | "Next slide" | Next arrow aria-label. |
CarouselAutoplaySettings
Section titled “CarouselAutoplaySettings”| Field | Type | Default | Description |
|---|---|---|---|
active | boolean? | true | Enable autoplay. |
delay | number? | 4000 | Milliseconds between slides. |
stopOnMouseEnter | boolean? | false | Pause when the pointer enters the carousel. |
stopOnFocusIn | boolean? | false | Pause when a child element receives focus. |
stopOnInteraction | boolean? | true | Stop permanently after user interacts. |
playOnInit | boolean? | true | Begin playing immediately on mount. |
breakpoints | CarouselAutoplayBreakpoints? | — | Media-query keyed overrides for autoplay behavior. |
CarouselNavigationState
Section titled “CarouselNavigationState”| Field | Type | Description |
|---|---|---|
activeIndex | number | Current active slide/page index. |
slideCount | number | Total number of slides. |
pageCount | number | Total snap pages (depends on grouping). |
canGoToPrev | boolean | Whether backward navigation is possible. |
canGoToNext | boolean | Whether forward navigation is possible. |
CarouselApiEvent
Section titled “CarouselApiEvent”| Field | Description |
|---|---|
select | Fired when the selected slide/page changes. |
reInit | Fired when the carousel reinitializes, such as after resize. |
autoplay:play | Fired when autoplay starts or resumes. |
autoplay:stop | Fired when autoplay stops or is paused. |
CarouselApi (ref)
Section titled “CarouselApi (ref)”| Method | Type | Description |
|---|---|---|
getActiveIndex | () => number | Get current active slide index. |
getSlideCount | () => number | Get total slide count. |
getPageCount | () => number | Get total snap page count. |
goTo | (index, opts?) => void | Navigate to a specific slide. |
goToNext | (opts?) => void | Navigate to next slide. |
goToPrev | (opts?) => void | Navigate to previous slide. |
canGoToNext | () => boolean | Whether forward navigation is possible. |
canGoToPrev | () => boolean | Whether backward navigation is possible. |
autoplay | CarouselAutoplayApi? | Autoplay control object. undefined when autoplay is inactive. |
CarouselAutoplayApi
Section titled “CarouselAutoplayApi”| Method | Type | Description |
|---|---|---|
play | (opts?) => void | Resume autoplay. |
stop | () => void | Stop autoplay. |
reset | () => void | Reset the autoplay timer. |
isPlaying | () => boolean | Whether autoplay is currently running. |
timeUntilNext | () => number | null | Milliseconds until the next slide, or null. |
Accessibility
Section titled “Accessibility”The Carousel renders with full ARIA semantics when interactive:
- The root element has
role="region"witharia-roledescription="carousel"and an accessible name viaaria-labeloraria-labelledby. - Each slide has
role="group"witharia-roledescription="slide"and anaria-labelthat defaults to “Slide n of total” (overridable viaCarouselSlide.label). - Previous/next arrow buttons have configurable
aria-labelviaprevButtonLabelandnextButtonLabelin settings. - Default dot navigation uses
role="tablist"with individualrole="tab"andaria-selectedfor the active page. - Autoplay automatically pauses when
prefers-reduced-motion: reduceis active.