Carousel
Scroll-snap slider built on embla-carousel. The root owns the state; buttons, dots and items read it through context, and a focusable viewport plus a polite status region keep it keyboard- and screen-reader-friendly.
Preview
Live preview
Precision
Every token is measured, contrast-checked and documented.
Velocity
Copy-paste components that ship without a build step.
Restraint
Motion stays on transform and opacity, and honours reduced motion.
Slide 1 of 3
Usage
Examples
With dots and a callback
Precision
Velocity
Restraint
Slide 1 of 3
<Carousel
className="w-full max-w-lg"
onSlideChange={setIndex}
footer={<CarouselDots className="mt-4" />}
>
{items.map((item) => (
<CarouselItem key={item.id}>…</CarouselItem>
))}
</Carousel>Custom controls
Precision
Velocity
Restraint
Slide 1 of 3
<Carousel
hideButtons
footer={
<div className="mt-4 flex items-center justify-center gap-2">
<CarouselPrevious />
<CarouselNext />
</div>
}
>
{items.map((item) => (
<CarouselItem key={item.id}>…</CarouselItem>
))}
</Carousel>Custom parts with useCarousel
Precision
Velocity
Restraint
1 of 3
Slide 1 of 3
import { useCarousel } from "@/components/vert-ui/carousel"
function Counter() {
const { index, count, scrollTo } = useCarousel()
return (
<div className="mt-4 flex items-center justify-center gap-4">
<span>{index + 1} of {count}</span>
<button type="button" onClick={() => scrollTo(0, true)}>Back to start</button>
</div>
)
}
<Carousel footer={<Counter />}>
{items.map((item) => (
<CarouselItem key={item.id}>…</CarouselItem>
))}
</Carousel>Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | Slide direction; buttons reposition to the top edge when vertical. |
| loop | boolean | false | Wraps from the last slide back to the first. |
| slidesToScroll | number | 1 | How many slides each prev/next call advances. |
| defaultSlide | number | 0 | Initial slide index (uncontrolled). |
| onSlideChange | (index: number) => void | — | Fires with the selected slide index whenever it changes. |
| dragFree | boolean | false | Decouples scroll position from drag distance for a free-flowing feel. |
| hideButtons | boolean | false | Hides the built-in prev/next buttons — add your own labelled controls. |
| footer | ReactNode | — | Rendered below the viewport inside the carousel context — the right place for dots or custom controls, since direct children become slides. |
| CarouselPrevious / CarouselNext | ButtonHTMLAttributes | — | Default controls; disabled at each end and labelled "Previous slide" / "Next slide". |
| CarouselDots | HTMLAttributes | — | One button per slide — labelled "Go to slide n", with aria-current on the active dot. |
| useCarousel | () => CarouselContextValue | — | Hook for custom parts (counters, progress, thumbnails): index, count, canPrev/canNext, scrollPrev/scrollNext/scrollTo and the embla API. Throws outside a Carousel. |
Accessibility
Accessibility notes
- The root is role="region" with aria-roledescription="carousel" — always pass an aria-label ("Principles" above) so it is a named landmark.
- With more than one slide the viewport is focusable: Arrow keys move along the carousel's axis, Home/End jump to the first/last slide. Keys are ignored while focus sits in an input or editable inside a slide.
- Slides carry role="group" with aria-roledescription="slide", so each one is identified while browsing.
- Previous/Next are real <button> elements with aria-label="Previous slide" / "Next slide", visible focus rings and the disabled attribute at each end.
- Dots are buttons with accessible names ("Go to slide n") and aria-current on the active one — a second way to move, never the only way.
- A visually hidden status region (aria-live="polite") announces "Slide x of y" after every move, including keyboard and dot navigation.
- Motion is transform-only (embla translates the container); under prefers-reduced-motion embla's animation duration drops to 0 and the global override kills CSS transitions.
- Never auto-advance: a carousel that moves on its own breaks focus and reading position.