DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Carousel

Touch-friendly slider with slide, card, fade and free-scroll variants. Built on Embla with autoplay, a real rotation control, keyboard navigation, RTL and vertical support.

When not to use a carousel

Worth reading before the API, because it is the part most carousel documentation leaves out.

More first-party design systems refuse to ship a carousel than ship one. Base UI, MUI, Adobe Spectrum, Polaris, PatternFly, Atlassian and Primer have none. USWDS filed the request to prohibit them as "our first anti-feature request". GOV.UK removed its homepage carousel in 2012 after testing found users did not scroll past it. Carbon for IBM.com ships one but forbids autoplay and looping outright, citing WCAG 2.2.2.

The recurring finding is that content below the first slide is rarely seen. So:

  • Do not put anything important on slide two. If every item matters, use a grid or a list — both are scannable at a glance and need no interaction.
  • Prefer a carousel for browsing, not for deciding. Galleries, related products, editorial rails: good. A comparison someone must reason about: bad.
  • Keep it short. Five frames or fewer is the usability recommendation. Past about eight, dots stop being countable — use CarouselCounter.
  • Give each carousel a real name describing what it holds, and do not stack two under one heading.

Examples

Default

A full-width slide carousel with looping, arrow nav and dot indicators.

Card

The card variant scales and fades adjacent slides for a centre-focused look.

Fade

Crossfades between slides — for testimonials and hero rotations, where a side-scroll would feel jarring.

Multi-peek

Fixed-width items leave the next slide cut off at the edge. That partial slide is the strongest discoverability cue a carousel has on a phone — stronger than dots.

Free-scroll rail

variant="scroll" is the momentum rail: no snapping, no arrows, no dots. The shape used by story rows and category rails.

Autoplay and the rotation control

Counter

Past about eight slides, dots stop working. A fraction scales to any length.

Dot variants and the ceiling

Clickable dots carry real target floors — 24px pitch on desktop, 44px on phones — so the row spreads with them. interactive={false} drops the targets and the floors with them: the tight indicator pitch holds on every width, and swipe, arrows and keyboard navigate. That is the form the showcase examples on this page use.

Nav placement

All three layouts below use the same position="inline" — flanking the viewport, below it beside the dots, or parked in a section header. Only the surrounding markup differs, so there is no prop for this.

On a coloured surface

Vertical

RTL

Accessibility

Loading

Installation

npx shadcn@latest add https://tessinaui.com/r/carousel.json

Usage

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  CarouselDots,
} from "@tessinaui/ui/carousel";

<Carousel size="full" loop aria-label="Featured products">
  <CarouselContent>
    <CarouselItem>Slide 1</CarouselItem>
    <CarouselItem>Slide 2</CarouselItem>
    <CarouselItem>Slide 3</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
  <CarouselDots />
</Carousel>

aria-label is required in practice: a role="region" with no name is not exposed as a landmark at all. Describe what the carousel contains, and do not include the word "carousel" — aria-roledescription already says that, and repeating it makes screen readers announce "Featured products, carousel, carousel". The component dev-warns when the name is missing.

API

Carousel

PropTypeDefaultDescription
variant"default" | "card" | "fade" | "scroll""default"card scales adjacent slides, fade crossfades, scroll is a free-momentum rail
size"sm" | "md" | "lg" | "full" | "auto""full"Default item width — sm=200px, md=300px, lg=420px, full=viewport, auto=content
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""none"Corner scale — 0/6/8/12/16/24px on panels; full caps at 24px per the panel rule
tone"default" | "on-color""default"Switches arrows and dots to currentColor for a coloured or photographic surface
orientation"horizontal" | "vertical""horizontal"Scroll axis. Vertical requires a height on the carousel
gap"none" | "sm" | "md" | "lg" | "xl""md"Spacing between items (0/8/16/24/32px), applied as logical properties
align"start" | "center" | "end""start"Where snap points align within the viewport
loopbooleanfalseInfinite loop
dragFreebooleanvariant === "scroll"Momentum scrolling with no snapping
autoplayboolean | numberfalsetrue = 4000ms, or a number of ms. Pair it with CarouselPlayPause
keyboardbooleantrueArrow / Home / End on the viewport, and what makes the viewport focusable
wheelbooleantrueTrackpad and wheel scrolling along the carousel's axis; the scroll rail also remaps the cross axis for mouse wheels
dir"ltr" | "rtl"inheritedStamped only when set; otherwise the document's direction is inherited
roleDescriptionstring"carousel"Localisable — no screen reader translates aria-roledescription
slideRoleDescriptionstring"slide"Localisable, as above
slideLabel(index, count) => string`${i+1} of ${n}`Names each slide
slidesLabelstring"Slides"Accessible name for the focusable viewport
optsEmblaOptionsType—Pass-through to Embla
setApi(api: CarouselApi) => void—Receive the Embla instance for imperative control

CarouselItem

PropTypeDefaultDescription
sizeCarouselSizeinheritedOverride the carousel's size for this item
indexnumberrender orderPosition override. Only needed when slides are not rendered in source order

CarouselPrevious / CarouselNext

PropTypeDefaultDescription
navVariant"default" | "plain" | "ghost" | "solid""default"Quiet to loud: plain has no fill until hover, ghost is translucent, default is filled and bordered, solid is the primary fill. Overlay plain/ghost lock to white ink on a scrim in both themes — they sit on the slides, and theme ink is invisible over a photo in light mode
navSize"sm" | "md" | "lg""md"32/40/48px on desktop; 44×44 below md for WCAG 2.5.5
position"overlay" | "inline""overlay"overlay floats over the viewport; inline flows in normal layout

CarouselDots

PropTypeDefaultDescription
dotVariant"circle" | "pill" | "line""circle"Indicator shape
maxDotsnumber8Past this, the row scrolls and distant dots scale down instead of the row growing
labelstring"Choose slide to display"Accessible name for the dot group
interactivebooleantruefalse renders a decorative indicator (iOS/IKEA shape): tight pitch, no buttons, aria-hidden. The default's 24px pitch is the WCAG 2.5.8 floor for clickable dots — this is the one compliant way below it

CarouselCounter

PropTypeDefaultDescription
format(current, count) => string`${c} / ${n}`Text format

Rendered aria-hidden: the carousel's live region already announces the same position on change, and announcing it twice is worse than once.

CarouselPlayPause

PropTypeDefaultDescription
playLabelstring"Start slide rotation"Label while paused
stopLabelstring"Stop slide rotation"Label while rotating
navVariant / navSizeas abovenavSize="sm"Shares the nav button styling

Renders null when the carousel has no autoplay, so it is safe to leave in a composition whose autoplay is conditional. Render it before CarouselContent so it is the first tab stop inside the carousel, which is what APG asks for.

CarouselProgress

A thin rail that fills over the autoplay interval, driven by the plugin's own timer. With rotation off it shows position in the set instead. Decorative.

CarouselScrollbar

A scroll-position line. The thumb's width is the fraction of the rail you can see and its position is how far along you are — a map of the track rather than a clock, so it stays meaningful with no autoplay and with more slides than you could ever dot. This is the affordance a long free-scrolling row wants; IKEA, Skillshare and Shop all pair one with theirs. Decorative.

<Carousel variant="scroll" size="auto" aria-label="Categories">
  <CarouselContent>…</CarouselContent>
  <CarouselScrollbar className="mt-4" />
</Carousel>

CarouselSkeleton

PropTypeDefaultDescription
countnumber3Placeholder slides
size / rounded / gap / orientationas the carousel—Reads the same maps the real component does
aspectRatiostring"16/9"A carousel has no intrinsic height, so the placeholder must declare one
showDotsbooleanfalseDots row under the viewport
dotsInteractivebooleantrueMirrors CarouselDots' interactive — button-cell pitch vs the tight decorative indicator
labelstring—Silent unless set

useCarouselApi

const { setApi, current, count } = useCarouselApi();

<Carousel setApi={setApi} aria-label="Gallery" />
<p>{current + 1} / {count}</p>

Trackpad and wheel

Wheel support is on for every variant: a two-finger trackpad swipe along the carousel's axis moves it. Embla is transform-driven and has no native scroller to receive that gesture, so the official wheel plugin bridges it.

The variants differ only in how much of the wheel they take. Snapping carousels listen on their own axis alone — a vertical scroll over a horizontal carousel still scrolls the page, which is what keeps this from being the most complained-about carousel behaviour on the web. variant="scroll" also remaps the cross axis onto the rail, so a plain mouse wheel — which has no X axis — can move it. wheel={false} opts any carousel out.

Autoplay behaviour

The contract follows the ARIA APG, which is stricter than the engine's defaults:

  • Hover pauses and resumes. Moving the pointer away restarts rotation.
  • Keyboard focus stops it for good. Tabbing in is a signal that someone is reading; rotation does not resume unless they ask for it.
  • A pointer press stops it for good, same reasoning.
  • Reduced motion starts it paused. Under prefers-reduced-motion: reduce nothing rotates until the play button is pressed.
  • The live region goes silent while rotating, so an auto-advancing carousel never narrates itself over the rest of the page.

Autoplay alone does not satisfy WCAG 2.2.2 — a visible, keyboard-operable control does. Render CarouselPlayPause whenever you set autoplay.

Accessibility

  • The root is a region with aria-roledescription="carousel" and a name you supply. Missing names are dev-warned.
  • Each slide is a group with aria-roledescription="slide" and a name — "3 of 10" by default, per APG's fallback for slides with no unique name.
  • Slides that are not in view are inert, so hidden content stays out of both the tab order and the accessibility tree. A partially visible peeking slide stays interactive; only fully off-screen ones are taken out.
  • A polite live region announces the position after it settles, and is silent while autoplay runs.
  • Dots are buttons in a named group, marked with aria-current. They are not a tablist: that pattern demands roving focus and aria-selected, and half of it is worse than none.
  • Arrows are named and disable at the bounds.

Keyboard

KeyAction
TabMoves through the rotation control, the viewport, and the nav buttons
← / →Previous / next slide (horizontal)
↑ / ↓Previous / next slide (vertical)
Home / EndFirst / last slide

The viewport itself is focusable, which is what gives the arrow keys somewhere to land — a carousel whose slides hold no focusable content would otherwise be unreachable by keyboard entirely.

This is ours, not the engine's. Embla ships no keyboard navigation; earlier versions of this page claimed it did. Set keyboard={false} to drop both the bindings and the tab stop.

Assistive technology

ReaderStatus
VoiceOver (macOS)Verified for roles, names and slide position
NVDAUntested — cannot be run on the maintainers' machines
JAWSUntested, as above

aria-roledescription is not universally supported: NVDA announces it, JAWS announces it twice, VoiceOver is inconsistent and TalkBack ignores it. Treat it as an enhancement, never as the thing that tells a user this is a carousel.

Migration

ChangeBeforeAfter
Accessible namedefaulted to "Carousel"no default; pass aria-label, dev-warns without one
rounded scale0/4/6/8/9999px0/6/8/12/16/24px, xl added, full capped at 24px
Fade slidesrequired a hand-passed indexderived from render order
Autoplay after hoverstopped permanently, unrecoverablepauses and resumes; only focus or a press stops it for good
variant="scroll"native overflow scroll plus EmblaEmbla only; implies dragFree
Skeleton announcementalways announcedsilent unless label is passed

The rounded change is visual and affects existing carousels. Everything else either fixes a defect or adds API.

Notes for agents

  • Every part carries a stable data-slot: carousel, carousel-viewport, carousel-track, carousel-item, carousel-previous, carousel-next, carousel-dots, carousel-dot, carousel-counter, carousel-play-pause, carousel-progress, carousel-status, carousel-skeleton.
  • The active slide carries data-active; the root carries data-variant, data-orientation and data-tone.
  • Do not assert on slide visibility via CSS. Read inert or data-active.
  • For unbounded or generated content, keep loop={false}, use CarouselCounter rather than dots, and offer a "see all" link — an infinite carousel has no honest indicator and no end.
CardChart

On this page

When not to use a carouselExamplesDefaultCardFadeMulti-peekFree-scroll railAutoplay and the rotation controlCounterDot variants and the ceilingNav placementOn a coloured surfaceVerticalRTLAccessibilityLoadingInstallationUsageAPICarouselCarouselItemCarouselPrevious / CarouselNextCarouselDotsCarouselCounterCarouselPlayPauseCarouselProgressCarouselScrollbarCarouselSkeletonuseCarouselApiTrackpad and wheelAutoplay behaviourAccessibilityKeyboardAssistive technologyMigrationNotes for agents