Carousel
Material Design 3 Expressive Carousel displays a dynamic, scrollable list of visual items with keyline matrix masking, parallax movement, uncontained multi-aspect layouts, 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 (HorizontalMultiBrowseCarousel, HorizontalUncontainedCarousel), the Carousel component for React 19 and Next.js (App Router) provides fluid keyline masking, dynamic parallax offsets, and dual-render support for both React Server Components (RSC) and Client-Side Rendering (CSR).
Per the latest Material Design 3 Carousel Guidelines (including the November 2025 Uncontained Multi-Aspect Ratio specification), Carousels adapt item widths and aspect ratios based on content density, container viewport width, and spatial computing environments.
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 (--carousel-item-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 and physical motion.
- Show All Action Button: Compliant 48px touch-target button that toggles between horizontal carousel scrolling and vertical responsive grid layout.
- Navigation Controls: Directional arrow buttons for previous and next slide advance.
Layout Comparison
| Layout Variant | Best Used For | Scrolling Mode | Snap Behavior | Edge Behavior |
|---|---|---|---|---|
multi-browse | Visual browsing (photo feeds, product cards) | Snap-scrolling | Snaps to nearest valid keyline arrangement | Keyline matrix masking at leading/trailing edges |
uncontained | Highly-customized or text-heavy items | Default (free) | Stops anywhere without layout grid snapping | Bleeds past container edge |
uncontained-multi-aspect | Editorial galleries with mixed aspect ratios (Nov 2025 spec) | Default (free) | Free scrolling with varied item aspect ratios | Bleeds past container edge |
multi-aspect-ratio | Mixed aspect ratio proportions (16:9, 4:3, 1:1, 9:16) | Default (free) | Continuous scrolling with preserved aspect ratios | Contained or bleeding items |
hero | Spotlighting single featured items (leading large + trailing small) | Snap-scrolling | Mandatory single-item advance | Leading large item, trailing preview |
center-aligned-hero | Centered large spotlight item with flanking previews | Snap-scrolling | Snaps center on active large item | Balanced leading and trailing small previews |
full-screen | Immersive vertical stories or video feeds | Snap-scrolling | Mandatory vertical page snap | Edge-to-edge full container height |
Layout Variants & Demos
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 where items can stop at any position.
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.
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 or custom navigation).
Features & Motion Architecture
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:
- Small item width: Clamped between
40pxand56px. - Medium item width: Transition step between large focal item and small preview items.
- Large item width: Primary focal slide occupying the bulk of viewport width.
- Masking: As items cross keyline boundaries,
clip-path: inset(...) round 28pxsmoothly compresses items without distorting child content.
Scrolling Behavior: Default vs Snap-Scrolling
- Snap-scrolling:
multi-browse,hero,center-aligned-hero, andfull-screenuse mandatory snap-scrolling. Releasing a pointer drag settles at the nearest valid item anchor; keyboard arrow navigation advances item-by-item. - Default (Fluid) scrolling:
uncontained,uncontained-multi-aspect, andmulti-aspect-ratiouse standard fluid scrolling, allowing items to halt at any point along the track. - Terminal Keyline Expansion: Contained layouts resolve a terminal keyline matrix at the list tail so the final slide does not leave an unmasked gap.
Parallax Content Translation
Applies a subtle inner content translation (transform: translate3d(var(--carousel-content-parallax-x), 0, 0)) relative to scroll position to mimic Jetpack Compose parallax physics.
RSC & CSR Dual-Render API
- RSC Mode (
children): Pass direct React Nodes or Next.js<Image />components into<Carousel>without client serialization errors. Wrap slides with<CarouselItem index={i} totalItems={n}>. - CSR 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. Controlled "Show All" Route Delegation
<Carousel
layout="multi-browse"
showAllButton
onShowAllClick={() => router.push('/gallery/all')}
showAllLabel="View All"
>
{/* Slides */}
</Carousel>
Best Practices
Do
- Set
preferredItemWidthappropriately so image thumbnails and text remain legible across breakpoints. - Include an accessible
aria-labelor header title describing the carousel content. - 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 omit
showAllButton={true}when expecting uncontrolled Show All toggle button to display. - Don't override keyline scroll snapping with legacy fixed pixel offsets.
- Don't import from root package
@bug-on/m3-expressive; always import from subpath@bug-on/m3-expressive/layout.
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.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
Imported from @bug-on/m3-expressive/layout:
| Prop | Type | Default | Description |
|---|---|---|---|
layout | CarouselLayout | "multi-browse" | M3 layout variant (multi-browse, uncontained, uncontained-multi-aspect, multi-aspect-ratio, hero, center-aligned-hero, full-screen). |
defaultActiveIndex | number | 0 | Initial focal slide for uncontrolled state. |
activeIndex | number | — | Controlled focal slide index. |
onActiveIndexChange | (index: number) => void | — | Callback invoked after the carousel settles on a focal slide. |
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. Required for uncontrolled toggle. |
isExpanded | boolean | — | Controlled state for expanded Grid view. |
onShowAllClick | () => void | — | Callback fired when 'Show all' is clicked (controlled route delegation). |
showAllLabel | string | "Show all" | Label text for 'Show all' button. |
showControls | boolean | "auto" | false | Show previous/next navigation buttons. |
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 |
|---|---|---|---|
index | number | — | Zero-based index of item (required in RSC mode). |
totalItems | number | — | Total count of items (required in RSC mode). |
layout | CarouselLayout | "multi-browse" | Parent layout variant. |
aspectRatio | "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | number | — | Item aspect ratio constraint. |
width | number | — | Fixed width in px override. |
minContentWidth | number | — | Minimum inner content container width for parallax calculation. |
maskClipPercent | number | 0 | Dynamic clip percentage override. |
parallaxOffset | number | 0 | Inner parallax content translation in px. |
isFocused | boolean | false | Whether item is currently focused. |
isScrolling | boolean | false | Whether carousel track is actively scrolling. |
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. |
children | ReactNode | — | Optional custom overlay content. |