Carousels — Hero Expressive extension
components specs/components/carousels-expressive.kmd
Material 3 Expressive extension to base Hero carousel (specs/components/ carousels.kmd). Adds shape morphing between selected/peek items (square ↔ pill ↔ squircle) and gesture inertia driven by spring physics (motion.kmd R9.1). Connected to shape-library.kmd morph contract.
Quando esta spec se aplica
Triggers primários
- Add Hero Expressive variant to a carousel surface
Todos os triggers
- Apply Expressive look on Hero carousel surfaces
- Implement hero feed (Vista, Reel) with Expressive variant
Corpo da especificação
Spec — Carousels Hero Expressive
Strict extension to
carousels.kmd§Hero layout. Adds shape morph + spring-driven inertia. Base spec remains valid; Expressive opt-in per surface OR per preset.
Princípios
- Strict extension — base Hero layout from
carousels.kmdunchanged. - Shape morph items — selected item uses larger radius, peek items smaller.
- Spring inertia — gesture release follows spring decay, NOT linear.
- Width tokens — large item 64-90% viewport; peek 10-32%.
R1 — Anatomy (extension)
Base Hero per carousels.kmd. Expressive adds:
┌──┐ ┌──────────────────┐ ┌──┐
│ ◐│ │ ◯ │ │◐ │
│ │ │ ⬢⬡⬢ │ │ │ ← peek items (Cookie-7-ish) + selected (Pill-ish)
│ ◐│ │ ◯ │ │◐ │
└──┘ └──────────────────┘ └──┘
During swipe, shapes morph in real-time (corner radius interpolates).
R2 — Shape tokens
| Position | Default shape (per shape-library.kmd) | Corner radius |
|---|---|---|
| Selected (center large) | Pill or shape-corner-extra-large | 32dp+ |
| Peek (sides) | shape-corner-large | 16-24dp |
| Mid-transition | interpolated linear | varies |
During swipe gesture:
- Position 0% (center): selected shape full radius.
- Position 50% (mid-swipe): item halfway out, radius interpolates toward peek.
- Position 100% (off-screen): peek shape OR fully gone.
R3 — Spring inertia
Release gesture (swipe-release):
- Velocity captured; preserved into spring
motion-spatial-default. - Spring decay: snap to nearest item center (large OR peek).
- Bounce: subtle overshoot (max ±5dp) by default; configurable per preset.
Forbidden anti-pattern: linear decay OR friction-based (felt "slidey").
R4 — Width tokens
| Token | Range | Use |
|---|---|---|
hero-item-large | 64-90% viewport | Selected center |
hero-item-peek | 10-32% viewport | Sides |
hero-gap | 8-16dp | Between items |
Compact width-class: large 90% + peek 5% (almost only large). Medium/Expanded: large 64-70% + peek 18% each side.
R5 — Per-preset variation
| Preset | Hero Expressive style |
|---|---|
material3 | Subtle (radius delta 16dp; minimal bounce) |
material_expressive | Full (radius delta 32dp; bounce ±5dp) |
material2 | Disabled — falls back to baseline carousels |
terminal_classic | n/a (no carousels in terminal) |
brutalist | Sharp corners only; no morph; no bounce (instant snap) |
cyberpunk_neon | Default + glow on selected |
minimalist_mono | No radius variation; only width difference |
R6 — Surface bindings
| Surface | API |
|---|---|
| Flutter | Extension of KoderCarousel (per carousels.kmd); KoderCarousel(variant: hero, expressive: true) |
| Web | <koder-carousel variant="hero" expressive> |
| Compose | KoderCarousel(variant = Hero, expressive = true) |
| SwiftUI | idem |
| CLI / TUI | n/a |
R7 — Acessibilidade
- Items:
role="listitem"per base spec. - Selected:
aria-current="true". - Swipe alternative: arrow keys advance one item at a time.
- Reduced-motion: no shape morph; no bounce; snap direct (instant transition).
- Touch target: each item ≥ 48dp tap area.
R8 — Reduced-motion
- Shape morph: disabled (static shapes per position).
- Spring inertia: replaced by linear snap (no overshoot, no bounce).
- Item swap: instant.
R9 — i18n
Inherits from carousels.kmd. No new keys.
T-suite
- T1 Mount Hero Expressive: selected item visible center with
Pillshape; peek items sides withshape-corner-large. - T2 Swipe gesture: radius interpolates continuously during drag.
- T3 Release with velocity: spring decay; snaps to nearest center.
- T4 Bounce: subtle overshoot visible (~5dp) before settle in
material_expressive. - T5 Width tokens: Compact width-class → large 90%; Medium → 64% + 18% peeks.
- T6 Reduced-motion: no morph + no bounce; instant snap.
- T7 Keyboard: arrows advance one item.
- T8 Selected aria-current: assistive tech announces correctly.
- T9 Material2 preset: falls back to base carousels (no extension).
Cross-link
- Base:
carousels.kmd§Hero - Shape morph:
shape-library.kmdR2 - Motion:
motion.kmdR9.1 - Layout:
app-layout/window-size-classes.kmd(width tokens) - Refs: M3 Carousel guidelines (Expressive Hero)
Requirements (testable)
Requirement: Shape morph on selected and peek items {#req-carousel-hero-shape-morph}
The Hero Expressive carousel SHALL render the selected center item with an extra-large corner radius (Pill / shape-corner-extra-large, 32dp+) and the side peek items with a smaller radius (shape-corner-large, 16–24dp), and during a swipe gesture SHALL interpolate each item's corner radius continuously between its selected and peek values as its position changes.
Scenario: Mount shows selected and peek shapes
- GIVEN a Hero carousel mounted with the Expressive variant enabled
- WHEN the initial layout settles
- THEN the center item renders with a
Pill/shape-corner-extra-largeradius of at least 32dp - AND the side peek items render with a
shape-corner-largeradius between 16dp and 24dp
Scenario: Radius interpolates during drag
- GIVEN a mounted Hero Expressive carousel at rest
- WHEN the user drags the selected item halfway toward the peek position
- THEN the item's corner radius reports a value strictly between its selected radius and its peek radius
- AND the radius updates continuously as the drag position changes
Requirement: Spring-driven release inertia {#req-carousel-hero-spring-inertia}
On gesture release the Hero Expressive carousel SHALL carry the captured swipe velocity into a spring decay (motion-spatial-default) that snaps to the nearest item center, and SHALL NOT use linear or friction-based decay; in the material_expressive preset the settle SHALL include a subtle overshoot bounce of at most ±5dp before resting.
Scenario: Release snaps to nearest center via spring
- GIVEN a Hero Expressive carousel being dragged with non-zero velocity
- WHEN the user releases the gesture
- THEN the carousel follows a spring decay and settles with an item centered
- AND no linear or friction-based decay curve is used
Scenario: Bounce overshoot in expressive preset
- GIVEN a Hero Expressive carousel using the
material_expressivepreset - WHEN the user releases a swipe with velocity
- THEN the settling item overshoots its resting center by at most 5dp before coming to rest
Requirement: Width tokens per width-class {#req-carousel-hero-width-tokens}
The Hero Expressive carousel SHALL size the selected item using the hero-item-large token (64–90% of viewport) and peek items using the hero-item-peek token (10–32% of viewport), resolving to large 90% with 5% peek in the Compact width-class and large 64–70% with 18% peeks in the Medium/Expanded width-classes.
Scenario: Compact width-class sizing
- GIVEN a Hero Expressive carousel rendered in the Compact width-class
- WHEN layout is measured
- THEN the selected item occupies 90% of the viewport width
- AND each peek item occupies about 5% of the viewport width
Scenario: Medium width-class sizing
- GIVEN a Hero Expressive carousel rendered in the Medium width-class
- WHEN layout is measured
- THEN the selected item occupies between 64% and 70% of the viewport width
- AND each side peek occupies about 18% of the viewport width
Requirement: Preset-specific morph and fallback behavior {#req-carousel-hero-preset-variation}
The Hero Expressive carousel SHALL vary its morph and inertia by preset: the material2 preset SHALL disable the extension and fall back to the baseline carousel, and the brutalist preset SHALL use sharp corners with no morph, no bounce, and an instant snap.
Scenario: Material2 falls back to baseline
- GIVEN a Hero carousel configured with the
material2preset and Expressive requested - WHEN it mounts and the user swipes
- THEN no shape morph or spring bounce occurs
- AND the carousel behaves as the baseline Hero carousel from
carousels.kmd
Scenario: Brutalist uses instant snap
- GIVEN a Hero Expressive carousel using the
brutalistpreset - WHEN the user releases a swipe
- THEN items keep sharp corners with no radius interpolation
- AND the carousel snaps to the nearest item instantly with no overshoot bounce
Requirement: Accessibility, keyboard, and reduced-motion {#req-carousel-hero-a11y}
The Hero Expressive carousel SHALL expose the selected item with aria-current="true", SHALL advance exactly one item per arrow-key press as a swipe alternative, SHALL provide a tap target of at least 48dp per item, and under reduced-motion SHALL disable shape morph and bounce and perform an instant direct snap instead of spring inertia.
Scenario: Keyboard advances one item
- GIVEN a focused Hero Expressive carousel with the selected item marked
aria-current="true" - WHEN the user presses the right arrow key once
- THEN the carousel advances by exactly one item
- AND the newly selected item now carries
aria-current="true"
Scenario: Reduced-motion disables morph and bounce
- GIVEN a Hero Expressive carousel with the reduced-motion preference enabled
- WHEN the user swipes and releases
- THEN no shape morph and no overshoot bounce occur
- AND the item swap resolves as an instant direct snap
Scenario: Tap target minimum size
- GIVEN a mounted Hero Expressive carousel
- WHEN each item's tap area is measured
- THEN every item exposes a tap target of at least 48dp
Referências
specs/components/carousels.kmdspecs/themes/motion.kmdspecs/themes/shape-library.kmd