MD3
Expressive
MATERIAL DESIGN 3 EXPRESSIVE

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 expressive 28px corner 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 VariantBest Used ForScrolling ModeSnap BehaviorEdge Behavior
multi-browseVisual browsing (photo feeds, product cards)Snap-scrollingSnaps to nearest valid keyline arrangementKeyline matrix masking at leading/trailing edges
uncontainedHighly-customized or text-heavy itemsDefault (free)Stops anywhere without layout grid snappingBleeds past container edge
uncontained-multi-aspectEditorial galleries with mixed aspect ratios (Nov 2025 spec)Default (free)Free scrolling with varied item aspect ratiosBleeds past container edge
multi-aspect-ratioMixed aspect ratio proportions (16:9, 4:3, 1:1, 9:16)Default (free)Continuous scrolling with preserved aspect ratiosContained or bleeding items
heroSpotlighting single featured items (leading large + trailing small)Snap-scrollingMandatory single-item advanceLeading large item, trailing preview
center-aligned-heroCentered large spotlight item with flanking previewsSnap-scrollingSnaps center on active large itemBalanced leading and trailing small previews
full-screenImmersive vertical stories or video feedsSnap-scrollingMandatory vertical page snapEdge-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.

Loading demo...

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.

Loading demo...

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.

Loading demo...

Full-Screen Vertical Layout

Edge-to-edge vertical scrolling carousel with mandatory snap-scrolling. Ideal for full-bleed video feeds or immersive story presentations.

Loading demo...

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.

Loading demo...

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).

Loading demo...

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 40px and 56px.
  • 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 28px smoothly compresses items without distorting child content.

Scrolling Behavior: Default vs Snap-Scrolling

  • Snap-scrolling: multi-browse, hero, center-aligned-hero, and full-screen use 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, and multi-aspect-ratio use 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

tsx
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 preferredItemWidth appropriately so image thumbnails and text remain legible across breakpoints.
  • Include an accessible aria-label or header title describing the carousel content.
  • Use CarouselItemText for readability over visual imagery.
  • Use hero layout 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

TokenValueDescription
--carousel-item-shape28pxM3 extraLarge shape corner radius
--carousel-item-spacing8pxDefault gap between adjacent carousel items
--carousel-content-padding16pxLeading and trailing container padding
--carousel-vertical-padding8pxTop and bottom container padding
--carousel-small-item-min40pxMinimum width limit for small preview items
--carousel-small-item-max56pxMaximum width limit for small preview items

Interactive Overlay Opacities

State LayerOpacity TokenColor Token
Hover Layer0.08var(--md-sys-color-on-surface)
Focus Layer0.1var(--md-sys-color-on-surface)
Pressed Layer0.1var(--md-sys-color-on-surface)
Focus Indicator3px thickness, 2px offsetvar(--md-sys-color-secondary)

Accessibility

  • Container Role: Renders role="region" with aria-roledescription="carousel" and accessible label.
  • Item Roles: Renders role="group" with aria-roledescription="slide" and aria-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

Imported from @bug-on/m3-expressive/layout:

PropTypeDefaultDescription
layoutCarouselLayout"multi-browse"M3 layout variant (multi-browse, uncontained, uncontained-multi-aspect, multi-aspect-ratio, hero, center-aligned-hero, full-screen).
defaultActiveIndexnumber0Initial focal slide for uncontrolled state.
activeIndexnumber—Controlled focal slide index.
onActiveIndexChange(index: number) => void—Callback invoked after the carousel settles on a focal slide.
childrenReactNode—RSC Mode: Direct React nodes or server components.
itemsT[]—CSR Mode: Data array.
renderItem(item: T, index: number) => ReactNode—CSR Mode: Render callback for item rendering.
preferredItemWidthnumber300Preferred width for large items in px.
itemSpacingnumber8Gap between adjacent items in px.
contentPaddingnumber16Leading/trailing container padding in px.
verticalPaddingnumber8Top/bottom container padding in px.
showAllButtonbooleanfalseWhether to display the "Show all" button. Required for uncontrolled toggle.
isExpandedboolean—Controlled state for expanded Grid view.
onShowAllClick() => void—Callback fired when 'Show all' is clicked (controlled route delegation).
showAllLabelstring"Show all"Label text for 'Show all' button.
showControlsboolean | "auto"falseShow previous/next navigation buttons.
prevButtonAriaLabelstring"Previous slide"Accessible label for previous slide button.
nextButtonAriaLabelstring"Next slide"Accessible label for next slide button.
userScrollEnabledbooleantrueEnable or disable user gestures/scrolling.
aria-labelstring"Content carousel"Accessible title/label for carousel container.

CarouselItem

PropTypeDefaultDescription
indexnumber—Zero-based index of item (required in RSC mode).
totalItemsnumber—Total count of items (required in RSC mode).
layoutCarouselLayout"multi-browse"Parent layout variant.
aspectRatio"16:9" | "4:3" | "1:1" | "3:4" | "9:16" | number—Item aspect ratio constraint.
widthnumber—Fixed width in px override.
minContentWidthnumber—Minimum inner content container width for parallax calculation.
maskClipPercentnumber0Dynamic clip percentage override.
parallaxOffsetnumber0Inner parallax content translation in px.
isFocusedbooleanfalseWhether item is currently focused.
isScrollingbooleanfalseWhether carousel track is actively scrolling.
disabledbooleanfalseDisables interaction and dims item.

CarouselItemText

PropTypeDefaultDescription
labelTextstring—Primary title text (Title Medium M3 typography).
supportingTextstring—Supporting text (Label Small M3 typography).
showScrimbooleantrueWhether to render the bottom gradient scrim.
childrenReactNode—Optional custom overlay content.