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.
Selection Modes
Single-select (radio buttons) or multi-select (checkboxes) lists. Clicking anywhere on the item or the indicator toggles selection state.
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.
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.
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).
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_SPRINGtier) for instant tactile feedback. - Effects Spring: Controls color background transitions and state overlay opacities (
DEFAULT_EFFECTS_SPRINGtier).
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>
Drag & Drop with @dnd-kit (Recommended)
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>
);
}
Custom Wrappers & Link Items
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:
- Attach
_m3ListItem = trueto your component function (or type it withListItemComponent) so<List>detects it as a valid item. - Forward
_listIndexandpositiondown 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.
| State | CSS Token | Value | Corner Level |
|---|---|---|---|
| Resting | --md-sys-shape-corner-extra-small | 4px | Extra Small |
| Hovered | --md-sys-shape-corner-medium | 12px | Medium |
| Focused / Pressed | --md-sys-shape-corner-large | 16px | Large |
| Selected / Dragged | --md-sys-shape-corner-large | 16px | Large |
| Disabled | --md-sys-shape-corner-extra-small | 4px | Extra Small |
Container Heights
Heights align strictly with the MD3 vertical spacing specs:
| Complexity | Layout / Configuration | Target Height |
|---|---|---|
| One-line | Label text only, with/without leading icons | 56px |
| Two-line | Label + 1 line of supporting text | 72px |
| Three-line | Label + 2 lines of supporting text / overline | 88px |
Motion & Springs
Transitions use organic spring physics to communicate state shifts.
| Transition | Spring Token | Tier |
|---|---|---|
| Shape/Size (Morphing) | FAST_SPATIAL_SPRING | Fast |
| Color/Opacity (State overlays) | DEFAULT_EFFECTS_SPRING | Default |
Accessibility
- Keyboard Navigation (Vertical): Press
ArrowUp/ArrowDownto cycle focus through interactive items. Focus automatically wraps around. - Keyboard Navigation (Horizontal): In
multi-actionmode, pressArrowLeft/ArrowRightto 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"andaria-selectedfor selection lists, androle="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 viatabIndex={-1}.
API Reference
List
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
value | string | string[] | — | Controlled selection value. |
defaultValue | string | string[] | — | Uncontrolled selection value initial state. |
onChange | (value: string | string[]) => void | — | Fired when selection state changes. |
outerRadius | number | 16 | Custom outer corner radius (px) for leading/trailing list items in expressive variant. |
innerRadius | number | 4 | Custom inner corner radius (px) for contiguous list items in expressive variant. |
expanded | string[] | — | Controlled values of currently expanded items. |
defaultExpanded | string[] | — | Initial expanded item values for uncontrolled usage. |
onExpandedChange | (expandedValues: string[]) => void | — | Fired when any item expands or collapses. |
ListItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | Required | Unique identifier for selection and sorting. |
headline | ReactNode | Required | Principal list item text. |
supportingText | ReactNode | — | Supplementary description. |
supportingTextLines | 1 | 2 | 1 | Max lines for supporting description. |
overline | ReactNode | — | Small text appearing above headline. |
leadingType | "none" | "icon" | "avatar" | "image" | "video" | "checkbox" | "radio" | "custom" | "none" | Leading layout configuration. |
leadingSrc | string | — | Image or video source link. |
leadingAlt | string | — | Alternative label for leading image. |
leadingContent | ReactNode | — | Content used for icon/avatar or custom. |
trailingType | "none" | "icon" | "icon-button" | "text" | "checkbox" | "radio" | "switch" | "custom" | "none" | Trailing layout configuration. |
trailingText | string | — | Trailing status text. |
trailingContent | ReactNode | — | Custom element or switch overrides. |
disabled | boolean | false | Disables interaction and reduces opacity. |
selected | boolean | — | Manual selected state override. |
href | string | — | Renders item as a link <a>. |
onClick | (event: React.MouseEvent<HTMLElement>) => void | — | Event callback. |
interactive | boolean | false | Forces interactive state (ripples & hover overlays) when no event or link is provided. |
dragHandle | boolean | false | Enables drag handle icon at trailing slot. Attach sensor listeners using dragHandleProps. |
isDragging | boolean | false | Explicitly marks item as dragging. Activates solo corner radius morphing and secondary-container background. |
dragHandleProps | HTMLAttributes<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. |
expandable | boolean | false | Enables expand/collapse behavior. Automatically renders animated rotating chevron when trailingType is none. |
expanded | boolean | — | Controlled expanded state. |
defaultExpanded | boolean | false | Initial 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). |
children | ReactNode | — | Nested sub-items or expandable content. |
ListItemGroup
| Prop | Type | Default | Description |
|---|---|---|---|
indent | string | "pl-4" | Custom indentation CSS class or style for nested sub-items. |
parentPosition | "solo" | "leading" | "middle" | "trailing" | — | Internal position of the parent list item. |
children | ReactNode | — | Nested sub-items. |
ListDivider
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Indents 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.