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
- Add a carousel
All triggers
- Browse a small set of items horizontally (5-20)
- Showcase a hero item with previews of next ones
- Replace a horizontal scroll of cards with a snap-based one
Specification body
Spec — Carousels
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/carousels.
3 layout patterns
| Layout | Item count visible | Use |
|---|---|---|
| Hero | 1 large + 1 preview edge | Featured / spotlight ("Featured project") |
| Multi-browse | 3-5 mixed sizes | Browse-heavy ("Recent files") |
| Contained | 2-4 equal sizes | Card-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.
| Slot | Width | Position |
|---|---|---|
| Large | 200 dp | Center / start |
| Medium | 140 dp | Trailing large |
| Small | 80 dp | At 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 width | Items visible | Item 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.kmdemphasized-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 count | Indicator |
|---|---|
| ≤ 10 | One dot per item |
| 11-30 | Grouped 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
| Surface | Controls |
|---|---|
| Mobile | Swipe + dots (no arrow buttons) |
| Tablet portrait | Swipe + dots |
| Desktop | Arrow 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-labelor visible heading) - Loading state: skeleton matching item shape until content loads
R8 — Accessibility
- Container:
role="region"+aria-roledescription="carousel"+aria-labeldescribing collection - Items:
role="group"+aria-roledescription="slide"+aria-label="3 of 7"(position + total) - Page indicator:
role="tablist"; each dotrole="tab"witharia-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
| Preset | Visual |
|---|---|
material3 | 12 px corners, edge peek, multi-size in Multi-browse |
material2 | 4 px corners, uniform sizes, no peek by default |
ios_cupertino | Page dots only (no arrows even on desktop), flush items |
gnome | Adwaita carousel with side fade, integrated arrow buttons |
windows_11 | Wide layout with arrow chevrons at sides, no dot indicator |
brutalist | Sharp corners, no peek, hard snap (no smooth scroll) |
terminal_classic | Sequential [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-motionhandling - ❌ Items with variable aspect ratios (jarring resize on scroll)
Cross-link
themes/motion.kmd— emphasized-decelerate easing for snapthemes/shape.kmd— item corner radiusthemes/color-roles.kmd— indicator dot colorcomponents/cards.kmd— item content patternapp-layout/window-size-classes.kmd— Contained items-visible rulesfoundations/elements.kmd— Container + Navigator families
Requirements (testable)
Requirement: Snap-to-item scroll settling {#req-carousel-snap}
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
Requirement: Keyboard navigation when focused {#req-carousel-keyboard}
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
Requirement: Accessible carousel semantics {#req-carousel-a11y-roles}
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
Requirement: Auto-advance pauses and honors configured interval {#req-carousel-auto-advance}
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
Requirement: Reduced-motion handling {#req-carousel-reduced-motion}
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
Requirement: Page indicator visibility for multi-item carousels {#req-carousel-indicator-visible}
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
specs/foundations/elements.kmdspecs/themes/motion.kmdspecs/themes/shape.kmdspecs/themes/color-roles.kmdspecs/components/cards.kmd