Carousel
Material Design 3 Expressive Carousel displays a dynamic, scrollable list of items with keyline matrix masking, parallax movement, and hybrid Show All controls.
Material Design 3 Expressive Carousels display a scrollable sequence of visual items. They dynamically interpolate item sizes across keyline matrix boundaries as content scrolls into and out of view.
Introduction
Ported directly from Jetpack Compose Material 3 Carousel specification, the Carousel component for React 19 and Next.js 19 (App Router) provides fluid keyline masking, dynamic parallax offsets, and dual-render support for both React Server Components (RSC) and Client-Side Rendering (CSR).
Anatomy
- Container (
role="region"): The primary horizontal or vertical scroll viewport (aria-roledescription="carousel"). - Carousel Items (
role="group"): Individual slide wrappers (aria-roledescription="slide") with expressive28pxcorner radius shape. - Keyline Mask Layer: Dynamic clip-path inset mask that smoothly expands and collapses items as they move across container edges.
- Parallax Content Container: Inner content container translating relative to scroll position to create visual depth.
- Show All Action Button: Compliant 48px touch-target button that toggles between horizontal carousel scrolling and vertical responsive grid layout.
Layout Variants
Multi-Browse Layout
Displays a mixture of Large, Medium, and Small items simultaneously. Items interpolate through the same keyline matrix at both edges of the viewport and snap to the nearest valid arrangement when released. Recommended for browsing photo galleries or visual content feeds.
Uncontained Layouts
Items maintain a fixed width or variable aspect ratio (16:9, 4:3, 1:1, 9:16) and bleed beyond the edge of the container. Supports default free scrolling.
Hero Layouts
Spotlights one primary Large item alongside Small preview items. Features mandatory single-item advance snap scrolling. center-aligned-hero centers the Large item with Small previews on both leading and trailing edges.
Full-Screen Vertical Layout
Edge-to-edge vertical scrolling carousel with mandatory snap-scrolling. Ideal for full-bleed video feeds or immersive story presentations.
Multi-Aspect Ratio Layout
Maintains standard Material Design 3 and Figma aspect ratio proportions (16:9, 4:3, 1:1, 3:4, 9:16) across carousel items.
XR Spatial Carousel
Designed for Android XR and spatial computing environments. Features an elevated glassmorphic panel (backdrop-blur-md, subtle border, spatial shadow) and floating orbiter navigation controls (showControls="orbiters").
Hybrid "Show All" Controls
Supports both Uncontrolled mode (internally toggling to a responsive display: grid with auto-fill columns when showAllButton is true) and Controlled mode (onShowAllClick callback delegating routing to Next.js Intercepting Routes).
Features
Dynamic Masking & Keyline Matrix
Calculates keylines dynamically based on container viewport width and preferredItemWidth. A cached geometry model drives each animation frame, so the scrolling position, item mask, parallax, and terminal keyline arrangement agree without reading item layout during drag. As items cross keyline boundaries, clip-path: inset(...) transitions them between Large, Medium, and Small sizes.
Scroll behavior by layout
multi-browse,hero,center-aligned-hero, andfull-screenuse mandatory snap-scrolling. Releasing a mouse drag settles at the nearest valid item anchor; touch and wheel input retain native scrolling and momentum.uncontained,uncontained-multi-aspect, andmulti-aspect-ratiouse free scrolling, so items can stop anywhere in the container.- Contained layouts resolve a terminal keyline matrix at the end of the list so the final item does not leave a visual gap after masking.
Parallax Movement
Applies a subtle inner content translation (transform: translateX(...)) relative to scroll position to mimic Jetpack Compose parallax scroll physics.
RSC & CSR Dual-Render API
- RSC / Next.js Image Mode (
children): Pass direct React Nodes or Next.js<Image />components into<Carousel>without client serialization errors. - CSR / Data-driven Mode (
items + renderItem): Pass an array of data items and a render callback.
Item Text Overlay & Scrim (CarouselItemText)
Includes an accessible text overlay with title medium and label small typography over a gradient scrim, matching the official M3 and Figma specifications.
Reduced Motion Handling
Automatically respects system @media (prefers-reduced-motion: reduce) settings. When enabled, inner parallax movements and dynamic keyline masks are disabled.
Usage
import { Card, Carousel, CarouselItem, CarouselItemText } from "@bug-on/m3-expressive/layout";
// 1. Standard RSC Mode with Item Text Overlay
<Carousel layout="multi-browse" preferredItemWidth={300} aria-label="Destinations">
<CarouselItem index={0} totalItems={2}>
<img src="/photo1.jpg" alt="Mountain" className="w-full h-full object-cover" />
<CarouselItemText labelText="Mountain Retreat" supportingText="Alpine Adventure" />
</CarouselItem>
<CarouselItem index={1} totalItems={2}>
<img src="/photo2.jpg" alt="Ocean" className="w-full h-full object-cover" />
<CarouselItemText labelText="Ocean Breeze" supportingText="Coastal Escape" />
</CarouselItem>
</Carousel>
// 2. XR Spatial Mode with Orbiters
<Carousel
variant="xr"
layout="multi-browse"
showControls="orbiters"
aria-label="Spatial Gallery"
items={myItemsArray}
renderItem={(item, index) => (
<Card variant="elevated">
<h4>{item.title}</h4>
<p>{item.description}</p>
</Card>
)}
/>
// 3. Controlled "Show All" Route Delegation
<Carousel
layout="multi-browse"
showAllButton
onShowAllClick={() => router.push('/gallery/all')}
showAllLabel="View All"
>
{/* Slides */}
</Carousel>
Best Practices
Do
- Set the
preferredItemWidthappropriately so image thumbnails and text remain legible. - Include an accessible
aria-labelor header title describing the carousel content. - Use
variant="xr"in spatial or floating panel contexts withshowControls="orbiters". - Use
CarouselItemTextfor readability over visual imagery. - Use
herolayout when spotlighting featured single items. - Keep compact contained-carousel text to two lines or fewer; use concise labels when an item reaches the small keyline.
Don't
- Don't shrink small item widths below
40px(min range: 40px to 56px). - Don't force
showAllButtonunless the user needs to view the complete catalog as a grid. - Don't override keyline scroll snapping with legacy fixed pixel offsets.
Design Tokens
Dimensional Tokens
| Token | Value | Description |
|---|---|---|
--carousel-item-shape | 28px | M3 extraLarge shape corner radius |
--carousel-item-spacing | 8px | Default gap between adjacent carousel items |
--carousel-content-padding | 16px | Leading and trailing container padding |
--carousel-vertical-padding | 8px | Top and bottom container padding |
--carousel-small-item-min | 40px | Minimum width limit for small preview items |
--carousel-small-item-max | 56px | Maximum width limit for small preview items |
Interactive Overlay Opacities
| State Layer | Opacity Token | Color Token |
|---|---|---|
| Hover Layer | 0.08 | var(--md-sys-color-on-surface) |
| Focus Layer | 0.1 | var(--md-sys-color-on-surface) |
| Pressed Layer | 0.1 | var(--md-sys-color-on-surface) |
| Focus Indicator | 3px thickness, 2px offset | var(--md-sys-color-secondary) |
Accessibility
- Container Role: Renders
role="region"witharia-roledescription="carousel"and accessible label. - Item Roles: Renders
role="group"witharia-roledescription="slide"andaria-label="Item X of Y". - Keyboard Support:
Tab: Moves focus into the active carousel item (index 0).ArrowLeft/ArrowRight: Navigates to previous or next slide and scrolls it into view.ArrowUp/ArrowDown: Exits carousel navigation to move focus out to adjacent page controls.Space/Enter: Activates the currently focused carousel slide.
- Show All Target: Minimum touch target size of 48×48px.
API Reference
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "standard" | "xr" | "standard" | Display style variant (standard flat or XR glassmorphic spatial panel). |
layout | CarouselLayout | "multi-browse" | M3 layout variant (multi-browse, uncontained, uncontained-multi-aspect, multi-aspect-ratio, hero, center-aligned-hero, full-screen). |
children | ReactNode | — | RSC Mode: Direct React nodes or server components. |
items | T[] | — | CSR Mode: Data array. |
renderItem | (item: T, index: number) => ReactNode | — | CSR Mode: Render callback for item rendering. |
preferredItemWidth | number | 300 | Preferred width for large items in px. |
itemSpacing | number | 8 | Gap between adjacent items in px. |
contentPadding | number | 16 | Leading/trailing container padding in px. |
verticalPadding | number | 8 | Top/bottom container padding in px. |
showAllButton | boolean | false | Whether to display the "Show all" button. |
isExpanded | boolean | — | Controlled state for expanded Grid view. |
onShowAllClick | () => void | — | Callback fired when 'Show all' is clicked. |
showAllLabel | string | "Show all" | Label text for 'Show all' button. |
showControls | boolean | "auto" | "orbiters" | false | Show previous/next buttons (orbiters in XR mode). |
prevButtonAriaLabel | string | "Previous slide" | Accessible label for previous slide button. |
nextButtonAriaLabel | string | "Next slide" | Accessible label for next slide button. |
userScrollEnabled | boolean | true | Enable or disable user gestures/scrolling. |
aria-label | string | "Content carousel" | Accessible title/label for carousel container. |
CarouselItem
| Prop | Type | Default | Description |
|---|---|---|---|
aspectRatio | "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | number | — | Item aspect ratio constraint. |
variant | "standard" | "xr" | "standard" | Item variant with optional XR hover/elevation. |
disabled | boolean | false | Disables interaction and dims item. |
CarouselItemText
| Prop | Type | Default | Description |
|---|---|---|---|
labelText | string | — | Primary title text (Title Medium M3 typography). |
supportingText | string | — | Supporting text (Label Small M3 typography). |
showScrim | boolean | true | Whether to render the bottom gradient scrim. |