Skip to content

Carousels

components specs/components/carousels.kmd

Horizontally scrolling stack of items, optimized for browsing a collection at a glance. Material parity (`/components/carousels`). Covers Hero, Multi-browse, and Contained layouts; snap behavior; swipe gestures; accessibility for sequential item navigation.

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Carousels

Facet Visual of Koder Design. Material parity: https://m3.material.io/components/carousels.

3 layout patterns

LayoutItem count visibleUse
Hero1 large + 1 preview edgeFeatured / spotlight ("Featured project")
Multi-browse3-5 mixed sizesBrowse-heavy ("Recent files")
Contained2-4 equal sizesCard-like grid in a strip

Pick by intent: Hero for "look at THIS"; Multi-browse for "scan across"; Contained for "uniform set you can scroll through".

Anatomy (Hero layout)

←─                                                      →─
   ┌──────────────────────────────────┐  ┌─────┐
   │                                  │  │     │
   │       Featured item              │  │ next│
   │      (full width minus           │  │ peek│
   │       16 px edge for peek)       │  │     │
   │                                  │  │     │
   └──────────────────────────────────┘  └─────┘
        ●  ○  ○  ○  ○                       ← page indicator
  • Item radius: 12 px (matches card defaults)
  • Item gap: 8 px
  • Edge peek: 16 px (shows ~24 px of next/previous item)
  • Page indicator: dots, 8 px circles, 4 px gap
  • Snap: item-edge to container-edge

R1 — Multi-browse layout

Mixed item widths in a single row. Material reference: large, medium, small repeating pattern.

SlotWidthPosition
Large200 dpCenter / start
Medium140 dpTrailing large
Small80 dpAt edges, indicates more

Items resize as they scroll across the visible area: focused = large, adjacent = medium, edges = small. Animation per scroll position.

R2 — Contained layout

Fixed item width, all equal. Width inferred from container:

Container widthItems visibleItem width
Compact (< 600 dp)1.2~80% of container
Medium (600-839 dp)2.2~45%
Expanded (≥ 840 dp)3.2~30%

The .2 partial visible signals scrollability.

R3 — Snap behavior

  • Snap to item: scroll-end snaps to nearest item edge
  • Velocity threshold: fast flick passes through multiple items; slow drag snaps to nearest
  • Disable snap: opt-out via prop when free-scroll is preferred (rare; document why)
  • Snap uses motion.kmd emphasized-decelerate easing (~350 ms)

R4 — Page indicator

Dots below carousel show:

  • Total item count (one dot per item, or grouped per "page" if > 10)
  • Current focus position (filled dot)
Item countIndicator
≤ 10One dot per item
11-30Grouped per visible page (e.g., 5 pages of 3 items)
> 30"1 / 47" text indicator instead of dots

Tappable dots: tap dot scrolls to that item (motion-medium, ~300 ms).

R5 — Navigation controls

SurfaceControls
MobileSwipe + dots (no arrow buttons)
Tablet portraitSwipe + dots
DesktopArrow buttons (◂ ▸) at edges + dots + keyboard

Arrow buttons appear on hover (default) OR always visible (opt-in). Disabled-state arrow when at start / end (38% opacity).

R6 — Keyboard navigation

When carousel has focus:

  • Arrow Left / Right: scroll to previous / next item (animated)
  • Home / End: jump to first / last
  • Tab: focus moves to focused item; Tab again moves OUT of carousel to the next focusable element after the carousel
  • Enter / Space on item: triggers item's action (open, etc.)

R7 — Item content

Items are normally cards or images. Each item must have:

  • A defined aspect ratio (16:9 / 4:3 / 1:1 — pick one per carousel)
  • A focusable target (whole item is the target by default)
  • An accessible name (aria-label or visible heading)
  • Loading state: skeleton matching item shape until content loads

R8 — Accessibility

  • Container: role="region" + aria-roledescription="carousel" + aria-label describing collection
  • Items: role="group" + aria-roledescription="slide" + aria-label="3 of 7" (position + total)
  • Page indicator: role="tablist"; each dot role="tab" with aria-selected="true" on current
  • Live region: when item changes via auto-advance, announce new item's name (polite live region)
  • Auto-advance ALWAYS pauses on hover, focus, or prefers-reduced-motion

R9 — Auto-advance (opt-in)

Off by default. When enabled:

  • Interval: 5-8 seconds per item (configurable; never < 4 s)
  • Pause on hover / focus / touch-down
  • Resume only after user interaction settles
  • Always pair with manual controls (dots + swipe)
  • Disabled when prefers-reduced-motion: reduce

Auto-advance is for promotional / passive carousels only. Don't auto-advance functional content (settings, lists).

R10 — Animation

  • Item scroll: 1:1 with finger drag; momentum on release
  • Snap settle: motion-emphasized-decelerate (~350 ms)
  • Arrow click: scroll by one item, motion-medium (~250 ms)
  • Multi-browse resize: items grow / shrink as they cross focus position (continuous interpolation)
  • Reduced motion: no resize animation, no auto-advance, instant snap

R11 — Per-preset variation

PresetVisual
material312 px corners, edge peek, multi-size in Multi-browse
material24 px corners, uniform sizes, no peek by default
ios_cupertinoPage dots only (no arrows even on desktop), flush items
gnomeAdwaita carousel with side fade, integrated arrow buttons
windows_11Wide layout with arrow chevrons at sides, no dot indicator
brutalistSharp corners, no peek, hard snap (no smooth scroll)
terminal_classicSequential [1/7] indicator + Arrow Left/Right navigation only

R12 — Forbidden patterns

  • ❌ Carousels with > 30 items (use grid / list instead)
  • ❌ Carousels as primary navigation (use tabs or nav components)
  • ❌ Hiding the page indicator entirely when count > 1 (user can't see "there's more")
  • ❌ Auto-advance < 4 s (too fast to read)
  • ❌ Auto-advance for forms or settings content
  • ❌ Carousel inside a horizontally-scrolling parent (nested horizontal scroll = unusable)
  • ❌ Carousel without keyboard navigation
  • ❌ Carousel without prefers-reduced-motion handling
  • ❌ Items with variable aspect ratios (jarring resize on scroll)
  • themes/motion.kmd — emphasized-decelerate easing for snap
  • themes/shape.kmd — item corner radius
  • themes/color-roles.kmd — indicator dot color
  • components/cards.kmd — item content pattern
  • app-layout/window-size-classes.kmd — Contained items-visible rules
  • foundations/elements.kmd — Container + Navigator families

Requirements (testable)

Per R3, when a scroll gesture ends the carousel SHALL settle so the nearest item edge aligns to the container edge, using the emphasized-decelerate easing (~350 ms), except when snap has been explicitly disabled via opt-out prop; a slow drag SHALL snap to the nearest item while a fast flick MAY pass through multiple items.

Scenario: Slow drag settles to nearest item edge

  • GIVEN a carousel with snap enabled and item edges at defined offsets
  • WHEN the user drags slowly and releases between two items
  • THEN the carousel animates to align the nearest item edge to the container edge
  • AND the settle animation uses emphasized-decelerate easing lasting approximately 350 ms

Scenario: Snap disabled allows free scroll

  • GIVEN a carousel rendered with the snap opt-out prop set
  • WHEN the user drags and releases between two items
  • THEN the scroll position is retained without snapping to any item edge

Per R6, when the carousel has focus it SHALL scroll to the previous item on Arrow Left and the next item on Arrow Right with an animated scroll, and it SHALL jump to the first item on Home and the last item on End; Enter or Space on the focused item SHALL trigger that item's action, and a second Tab SHALL move focus out of the carousel to the next focusable element after it.

Scenario: Arrow keys move between items

  • GIVEN a carousel that currently has focus with item 3 focused
  • WHEN the user presses Arrow Right
  • THEN the carousel animates to focus item 4
  • AND WHEN the user presses Home
  • THEN the carousel focuses the first item

Scenario: Enter triggers focused item action

  • GIVEN a carousel with focus on an item whose action opens a detail view
  • WHEN the user presses Enter
  • THEN the focused item's action is invoked

Per R8, the carousel container SHALL expose role="region" with aria-roledescription="carousel" and an aria-label describing the collection; each item SHALL expose role="group" with aria-roledescription="slide" and an aria-label giving its position and total (e.g. "3 of 7"); the page indicator SHALL be a role="tablist" whose dots are role="tab" with aria-selected="true" on the current one only.

Scenario: Container and item ARIA attributes present

  • GIVEN a rendered carousel of 7 items with the 3rd item focused
  • WHEN the accessibility tree is inspected
  • THEN the container has role region, aria-roledescription "carousel", and a non-empty aria-label
  • AND the focused item has role group, aria-roledescription "slide", and aria-label "3 of 7"

Scenario: Only the current dot is selected

  • GIVEN a carousel whose page indicator renders as a tablist of dots
  • WHEN the current focus position is item 3
  • THEN exactly one dot has aria-selected "true" and it corresponds to item 3

Per R9, auto-advance SHALL be off by default; when enabled its interval SHALL be configurable but never less than 4 seconds, and it SHALL pause on hover, focus, or touch-down and resume only after user interaction settles, always paired with manual controls.

Scenario: Auto-advance pauses on hover

  • GIVEN a carousel with auto-advance enabled at a 5 second interval
  • WHEN the pointer hovers over the carousel
  • THEN the auto-advance timer is paused and no item change occurs while hovering
  • AND WHEN the pointer leaves after interaction settles
  • THEN the auto-advance timer resumes

Scenario: Interval below floor is rejected

  • GIVEN a carousel configured with an auto-advance interval of 3 seconds
  • WHEN the carousel initializes
  • THEN the configuration is rejected or clamped so the effective interval is at least 4 seconds

Per R8, R9 and R10, when prefers-reduced-motion: reduce is active the carousel SHALL disable auto-advance, suppress the multi-browse resize animation, and perform snap instantly without a scroll animation.

Scenario: Reduced motion disables auto-advance and animation

  • GIVEN a carousel with auto-advance enabled and multi-browse resize
  • WHEN prefers-reduced-motion reduce is active
  • THEN auto-advance does not advance items automatically
  • AND item resize animation is not applied
  • AND snapping to an item occurs instantly with no scroll animation

Per R4 and R12, a carousel containing more than one item SHALL keep a position indicator visible so the user can perceive there is more content — dots per item at 10 or fewer, grouped-per-page dots at 11–30, and a "current / total" text indicator above 30 — and SHALL NOT hide the indicator entirely.

Scenario: Text indicator used above 30 items

  • GIVEN a carousel with 47 items
  • WHEN the carousel renders
  • THEN a text indicator of the form "1 / 47" is shown instead of dots
  • AND no dot-based indicator is rendered

Scenario: Indicator never fully hidden for multiple items

  • GIVEN a carousel with more than one item
  • WHEN the carousel renders
  • THEN a position indicator (dots or text) is present and visible

References