MD3
Expressive
MATERIAL DESIGN 3 EXPRESSIVE

Lists

Lists are continuous, vertical indexes of text or images. Expressive Lists feature shape morphing animations, multi-action support, and library-agnostic drag-and-drop readiness.

Lists are used to group related content or actions into a single column. The MD3 Expressive List features dynamic corner rounding (shape morphing) based on interaction states and native drag-and-drop readiness.

Introduction

Expressive Lists are designed to feel tactile and fluid. In the expressive variant, items transition their corner shapes from standard roundness (CornerExtraSmall - 4px) to dynamic bubbles (CornerMedium/CornerLarge - 12px/16px) upon hover, focus, and drag interaction.

Anatomy

  • Container: The list wrapper (either standard or segmented with gap).
  • Leading Slot: Supports icons, avatars, images, video previews, or selection indicators (checkboxes/radio buttons).
  • Content Area: Contains an optional overline, main headline, and supporting text (1 or 2 lines).
  • Trailing Slot: Contains action elements like text, switches, buttons, or a drag handle.
  • Divider: A separating line that can be full-width or inset starting after leading elements.

Variants

Basic Lists

Baseline and Expressive variants showing standard or segmented styles.

Loading demo...

Selection Modes

Single-select (radio buttons) or multi-select (checkboxes) lists. Clicking anywhere on the item or the indicator toggles selection state.

Loading demo...

Drag & Drop Integration

Expressive Lists provide native visual support for drag-and-drop interactions (isDragging, dragHandle, dragHandleProps) while staying zero-dependency to keep library bundle size minimal. We recommend pairing with @dnd-kit.

Loading demo...

Rich Media & Multi-Action

List items can hold complex elements like video previews (top-aligned) and support Multi-Action mode where the container and the trailing action have separate focus areas.

Loading demo...

Expandable & Nested Lists

Expandable lists allow organizing hierarchy by nesting sub-items inside a parent item. Clicking smoothly expands or collapses the child items with fluid spring animations. The corner radii of the parent and sub-items adaptively morph according to their position context (leading, middle, trailing, or solo).

Loading demo...

Features

Expressive Motion

Expressive Lists utilize physics-based spring animations for fluid state transitions. When a user hovers, focuses, or drags a list item, its corners dynamically expand from 4px (extra small shape) to 12px or 16px (medium/large shape). This shape morphing is governed by:

  • Spatial Spring: Manages the corner radius shape morphing and size changes (FAST_SPATIAL_SPRING tier) for instant tactile feedback.
  • Effects Spring: Controls color background transitions and state overlay opacities (DEFAULT_EFFECTS_SPRING tier).

Responsive Scaling & Adaptation

List layouts adapt dynamically across screen size classes to preserve comfortable reading line lengths:

  • Compact Window Sizes (under 600dp): List containers extend edge-to-edge. Selecting an item navigates to a full-screen detailed view.
  • Medium & Expanded Window Sizes (600dp - 1200dp+): Lists can adapt margins, scale down padding, or transition into a side-by-side list-detail view or a multi-column grid.
  • Line Length: Primary and supporting text is optimized for quick scanning, keeping the line length between 40 to 60 characters for maximum readability.

Usage

Basic Usage

import { List, ListDivider, ListItem } from "@bug-on/m3-expressive/layout";

<List variant="expressive">
  <ListItem
    value="item1"
    headline="Inbox"
    supportingText="Check your emails"
    leadingType="icon"
    leadingContent={<span className="material-symbols-rounded">inbox</span>}
    interactive
  />
  <ListDivider inset insetType="icon" />
  <ListItem
    value="item2"
    headline="Trash"
    supportingText="Deleted items"
    leadingType="icon"
    leadingContent={<span className="material-symbols-rounded">delete</span>}
    interactive
  />
</List>

Expandable & Nested List

import { List, ListItem } from "@bug-on/m3-expressive/layout";

<List variant="expressive" listStyle="segmented">
  <ListItem
    value="projects"
    headline="Projects"
    supportingText="2 active items"
    leadingType="icon"
    leadingContent={<span className="material-symbols-rounded">folder</span>}
    expandable
    defaultExpanded
  >
    <ListItem
      value="proj-phoenix"
      headline="Design System"
      supportingText="Expressive tokens"
      interactive
    />
    <ListItem
      value="proj-titan"
      headline="Mobile Client"
      supportingText="React Native"
      interactive
    />
  </ListItem>
  <ListItem value="inbox" headline="Inbox" interactive />
  <ListItem value="archive" headline="Archive" interactive />
</List>

To enable drag and drop sorting, install @dnd-kit/core and @dnd-kit/sortable in your application. Wrap <ListItem> with useSortable and forward the sensor listeners to dragHandleProps:

import {
  closestCenter,
  DndContext,
  type DragEndEvent,
  type DragStartEvent,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
} from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  List,
  ListItem,
  type ListItemComponent,
  type ListItemPosition,
} from "@bug-on/m3-expressive/layout";
import { useId, useState } from "react";

interface SortableItemProps {
  id: string;
  title: string;
  _listIndex?: number;
  position?: ListItemPosition;
  activeId?: string | null;
}

const SortableItem: React.FC<SortableItemProps> & ListItemComponent = ({
  id,
  title,
  _listIndex,
  position,
  activeId,
  ...props
}) => {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
    useSortable({ id });

  // Keep solo position and dragging elevation while actively dragged or transitioning
  const dragging = isDragging || activeId === id;

  return (
    <ListItem
      ref={setNodeRef}
      value={id}
      _listIndex={_listIndex}
      position={position}
      headline={title}
      dragHandle
      dragHandleProps={{ ...attributes, ...listeners }}
      isDragging={dragging}
      style={{
        transform: CSS.Transform.toString(transform),
        transition,
      }}
      {...props}
    />
  );
};
SortableItem._m3ListItem = true;

export function ReorderableListExample() {
  const dndId = useId();
  const [items, setItems] = useState([
    { id: "1", title: "Design System Architecture" },
    { id: "2", title: "Implement Lists Component" },
    { id: "3", title: "Ship Expressive v4" },
  ]);
  const [activeId, setActiveId] = useState<string | null>(null);

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  const handleDragStart = (event: DragStartEvent) => {
    setActiveId(String(event.active.id));
  };

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    if (over && active.id !== over.id) {
      setItems((prev) => {
        const oldIndex = prev.findIndex((item) => item.id === active.id);
        const newIndex = prev.findIndex((item) => item.id === over.id);
        return arrayMove(prev, oldIndex, newIndex);
      });
    }
    setActiveId(null);
  };

  const handleDragCancel = () => {
    setActiveId(null);
  };

  return (
    <DndContext
      id={dndId}
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
      onDragCancel={handleDragCancel}
    >
      <SortableContext
        items={items.map((item) => item.id)}
        strategy={verticalListSortingStrategy}
      >
        <List variant="expressive" listStyle="segmented">
          {items.map((item) => (
            <SortableItem
              key={item.id}
              id={item.id}
              title={item.title}
              activeId={activeId}
            />
          ))}
        </List>
      </SortableContext>
    </DndContext>
  );
}

In the MD3 Expressive variant, <List> automatically calculates asymmetric corner rounding (leading, middle, trailing, solo) by evaluating direct children and injecting _listIndex.

When wrapping <ListItem> in a custom component (e.g. <LinkCard> or a router <Link>), you must:

  1. Attach _m3ListItem = true to your component function (or type it with ListItemComponent) so <List> detects it as a valid item.
  2. Forward _listIndex and position down to the inner <ListItem>.
import {
  List,
  ListItem,
  type ListItemComponent,
  type ListItemProps,
} from "@bug-on/m3-expressive/layout";

interface LinkCardProps extends ListItemProps {
  href: string;
}

// 1. Forward _listIndex and position down to <ListItem>
export const LinkCard: React.FC<LinkCardProps> & ListItemComponent = ({
  _listIndex,
  position,
  href,
  headline,
  ...props
}) => {
  return (
    <a href={href} className="contents">
      <ListItem
        _listIndex={_listIndex}
        position={position}
        headline={headline}
        interactive
        {...props}
      />
    </a>
  );
};

// 2. Mark with static _m3ListItem flag
LinkCard._m3ListItem = true;

// Usage inside <List>
<List variant="expressive">
  <LinkCard href="/profile" value="profile" headline="User Profile" />
  <LinkCard href="/settings" value="settings" headline="Account Settings" />
  <LinkCard href="/logout" value="logout" headline="Log Out" />
</List>

Best Practices

Do

  • Align supporting visuals (such as icons, avatars, or images) consistently at the leading edge to maintain scannability.
  • Use segmented gaps (listStyle="segmented") and filled container items to clearly define contained lists.
  • Keep label text brief and limit supporting description text to 1 to 3 lines, allowing it to truncate based on screen size.
  • Provide descriptive aria-labels for interactive elements like custom trailing actions or drag handles.
  • Adjust container margins on large screens (e.g. tablet or desktop) to prevent overly long line lengths and maintain comfortable reading.

Don't

  • Don't vary the placement of visual elements (e.g. shifting icons from leading to middle) within the same list.
  • Don't use a high-emphasis design for repetitive secondary actions in multi-action lists.
  • Don't pair checkboxes with single-select lists, or radio buttons with multi-select lists.
  • Don't rely solely on background color changes to indicate selection states. Always pair color shifts with supporting visual cues (e.g. checkboxes, radio buttons, or selection checkmarks).

Design Tokens

Corner Radius

The Expressive List component implements MD3 Expressive shape morphing across state layers.

StateCSS TokenValueCorner Level
Resting--md-sys-shape-corner-extra-small4pxExtra Small
Hovered--md-sys-shape-corner-medium12pxMedium
Focused / Pressed--md-sys-shape-corner-large16pxLarge
Selected / Dragged--md-sys-shape-corner-large16pxLarge
Disabled--md-sys-shape-corner-extra-small4pxExtra Small

Container Heights

Heights align strictly with the MD3 vertical spacing specs:

ComplexityLayout / ConfigurationTarget Height
One-lineLabel text only, with/without leading icons56px
Two-lineLabel + 1 line of supporting text72px
Three-lineLabel + 2 lines of supporting text / overline88px

Motion & Springs

Transitions use organic spring physics to communicate state shifts.

TransitionSpring TokenTier
Shape/Size (Morphing)FAST_SPATIAL_SPRINGFast
Color/Opacity (State overlays)DEFAULT_EFFECTS_SPRINGDefault

Accessibility

  • Keyboard Navigation (Vertical): Press ArrowUp / ArrowDown to cycle focus through interactive items. Focus automatically wraps around.
  • Keyboard Navigation (Horizontal): In multi-action mode, press ArrowLeft / ArrowRight to transition focus between the primary item body and secondary trailing actions. Focus automatically skips disabled controls.
  • Initial Focus: When tabbed, focus lands on the first item. If the list has an active selected item, focus lands on the selected item first.
  • ARIA Roles: Automatically assigns role="listbox" and aria-selected for selection lists, and role="list" for actions.
  • Visual Cues: Selection is indicated by both a color background change and a checkbox/radio control to support users with low vision (WCAG 2.1 compliance).
  • Disabled State: Correctly assigns aria-disabled="true" and removes focus via tabIndex={-1}.

API Reference

List

PropTypeDefaultDescription
variant"baseline" | "expressive""baseline"Design scheme variant.
listStyle"standard" | "segmented""standard"Segmented list has 2px gap.
selectionMode"none" | "single-action" | "multi-action" | "single-select" | "multi-select""none"Changes interaction roles and keyboard routing.
valuestring | string[]—Controlled selection value.
defaultValuestring | string[]—Uncontrolled selection value initial state.
onChange(value: string | string[]) => void—Fired when selection state changes.
outerRadiusnumber16Custom outer corner radius (px) for leading/trailing list items in expressive variant.
innerRadiusnumber4Custom inner corner radius (px) for contiguous list items in expressive variant.
expandedstring[]—Controlled values of currently expanded items.
defaultExpandedstring[]—Initial expanded item values for uncontrolled usage.
onExpandedChange(expandedValues: string[]) => void—Fired when any item expands or collapses.

ListItem

PropTypeDefaultDescription
valuestringRequiredUnique identifier for selection and sorting.
headlineReactNodeRequiredPrincipal list item text.
supportingTextReactNode—Supplementary description.
supportingTextLines1 | 21Max lines for supporting description.
overlineReactNode—Small text appearing above headline.
leadingType"none" | "icon" | "avatar" | "image" | "video" | "checkbox" | "radio" | "custom""none"Leading layout configuration.
leadingSrcstring—Image or video source link.
leadingAltstring—Alternative label for leading image.
leadingContentReactNode—Content used for icon/avatar or custom.
trailingType"none" | "icon" | "icon-button" | "text" | "checkbox" | "radio" | "switch" | "custom""none"Trailing layout configuration.
trailingTextstring—Trailing status text.
trailingContentReactNode—Custom element or switch overrides.
disabledbooleanfalseDisables interaction and reduces opacity.
selectedboolean—Manual selected state override.
hrefstring—Renders item as a link <a>.
onClick(event: React.MouseEvent<HTMLElement>) => void—Event callback.
interactivebooleanfalseForces interactive state (ripples & hover overlays) when no event or link is provided.
dragHandlebooleanfalseEnables drag handle icon at trailing slot. Attach sensor listeners using dragHandleProps.
isDraggingbooleanfalseExplicitly marks item as dragging. Activates solo corner radius morphing and secondary-container background.
dragHandlePropsHTMLAttributes<HTMLElement>—Props forwarded to drag handle element (e.g. { ...attributes, ...listeners } from @dnd-kit/sortable).
position"solo" | "leading" | "middle" | "trailing"—Overrides the list item position for corner radius calculations.
expandablebooleanfalseEnables expand/collapse behavior. Automatically renders animated rotating chevron when trailingType is none.
expandedboolean—Controlled expanded state.
defaultExpandedbooleanfalseInitial expanded state for uncontrolled usage.
onExpandChange(expanded: boolean) => void—Callback fired when item expands or collapses.
expandTrigger"row" | "trailing""row"Area that toggles expansion ("row" for entire item body, "trailing" for chevron button only).
childrenReactNode—Nested sub-items or expandable content.

ListItemGroup

PropTypeDefaultDescription
indentstring"pl-4"Custom indentation CSS class or style for nested sub-items.
parentPosition"solo" | "leading" | "middle" | "trailing"—Internal position of the parent list item.
childrenReactNode—Nested sub-items.

ListDivider

PropTypeDefaultDescription
insetbooleanfalseIndents divider line after leading slot elements.
insetType"icon" | "avatar" | "custom""avatar"Sets indentation amount (56px for icon, 72px for avatar).

ListItemComponent

Marker type for custom list item wrappers:

export interface ListItemComponent {
  _m3ListItem: true;
}

Attach _m3ListItem = true to your wrapper function component so that <List> counts it as a list item and computes the proper asymmetric border-radius.