Loading…
Loading…
Displays a continuous group of related items vertically.
The List component displays a continuous, vertical group of related items. It is one of the oldest and most ubiquitous UI patterns — from email inboxes and settings panels to navigation menus and search results — the list is a foundational layout for structured data.
Unlike a Table which organizes data into rigidly defined columns, a list allows each item to have a flexible internal layout: a leading avatar, primary and secondary text lines, trailing metadata, and action controls. This flexibility makes lists ideal for content where items share a type but vary in structure.
When to use a List:
When NOT to use a List:
<dl>)Use the Spacing Tool to establish consistent padding and gap values between list items. Check text contrast with the Contrast Checker to ensure secondary text lines remain readable.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Simple | Basic text-only items. | Single line of text per item. Minimal height. |
| Two-line | Primary text with supporting description. | Primary text (bold) + secondary text (muted, smaller). |
| Three-line | Rich content items (e.g., email previews). | Primary text, secondary text, and a third metadata line. |
| Interactive | Clickable/selectable items (navigation, selection). | Hover background, cursor pointer, focus ring. |
| Grouped | Items organized under section headers. | Sticky or static subheaders dividing logical groups. |
| Ordered | Sequential items where order matters. | Rendered as <ol> with optional visible numbering. |
List items are composed of several optional slots:
| Slot | Position | Content | Example |
|---|---|---|---|
| Leading | Left | Avatar, icon, checkbox, thumbnail | User avatar, file type icon |
| Content | Center | Primary text, secondary text, metadata | Name, email, timestamp |
| Trailing | Right | Action button, badge, switch, chevron | Delete button, unread badge, toggle |
| Style | When to Use |
|---|---|
| Full-width | Between items of equal visual weight |
| Inset | Between items with leading content (divider aligns with text, not the icon) |
| None | Tight spacing with clear background alternation or sufficient padding |
Use the Spacing Tool to calculate inset divider offsets — typically the leading slot width (40px avatar + 16px gap = 56px inset).
| Property | Type | Default | Description |
|---|---|---|---|
variant | 'simple' | 'two-line' | 'three-line' | 'simple' | Content density per item |
divider | 'full' | 'inset' | 'none' | 'full' | Divider style between items |
interactive | boolean | false | Enables hover/focus states on items |
selectable | boolean | false | Enables single or multi-selection |
selectionMode | 'single' | 'multiple' | 'single' | Selection behavior when selectable |
dense | boolean | false | Reduces item padding for compact layouts |
disablePadding | boolean | false | Removes default list padding |
virtualized | boolean | false | Enables windowed rendering for large datasets |
as | 'ul' | 'ol' | 'nav' | 'div' | 'ul' | Root element type |
| Property | Type | Default | Description |
|---|---|---|---|
leading | ReactNode | — | Left-side content (avatar, icon, checkbox) |
trailing | ReactNode | — | Right-side content (action, badge, switch) |
primaryText | string | — | Main text label |
secondaryText | string | — | Supporting description text |
selected | boolean | false | Controlled selection state |
disabled | boolean | false | Prevents interaction |
href | string | — | Renders item as a link |
onClick | () => void | — | Click handler for interactive items |
Lists use a focused set of spacing and surface tokens. See the Design Tokens Guide for full reference.
| Token Category | Token Example | List Usage |
|---|---|---|
| Spacing – Padding | --space-3 (12px) / --space-4 (16px) | Item vertical/horizontal padding |
| Spacing – Dense Padding | --space-2 (8px) | Reduced padding in dense mode |
| Spacing – Gap | --space-3 (12px) | Gap between leading slot and content |
| Spacing – Inset | --space-14 (56px) | Inset divider left offset (avatar width + gap) |
| Color – Surface | --color-surface | List background |
| Color – Hover | --color-surface-hover | Interactive item hover state |
| Color – Selected | --color-primary-50 | Selected item background tint |
| Color – Primary Text | --color-text-primary | Item primary text |
| Color – Secondary Text | --color-text-secondary | Item secondary/description text |
| Color – Divider | --color-border-subtle | Divider line color |
| Border | 1px solid var(--color-border-subtle) | Divider between items |
| Typography | --font-size-sm, --font-size-xs | Primary / secondary text sizes |
| Transition | --duration-fast | Hover/selection background transitions |
Use the Spacing Tool to preview how different padding scales affect list density.
| State | Visual Change | Notes |
|---|---|---|
| Default | Standard appearance with dividers and content. | — |
| Hover (interactive) | Subtle background tint (surface-hover). | Only on interactive or selectable lists. |
| Focused | Focus ring or background highlight on the focused item. | Keyboard navigation focus must be clearly visible (WCAG 2.4.7). |
| Selected | Primary-tinted background, optional checkmark in leading slot. | Single or multi-select. Communicates selection via aria-selected="true". |
| Disabled | Muted text, no hover/click response. | aria-disabled="true" on the item. |
| Empty | Placeholder message ("No items to display") or Empty State. | Never render an empty <ul> with no feedback. |
| Loading | Skeleton placeholders mimicking item shape. | Show 3–5 skeleton items matching the expected item height. |
| Reorderable | Drag handle visible, item lifts on grab with shadow. | Requires ARIA live region announcements for drag position. |
Lists are semantically rich and must convey structure, interactivity, and state to assistive technologies.
WCAG 1.3.1 — Info and Relationships: Use proper list semantics. A collection of related items should be a <ul> or <ol>, each item a <li>. Navigation lists should be wrapped in a <nav> landmark. Grouped lists should use headings or aria-labelledby to associate section headers with their groups. Screen readers announce "list, 12 items" — this structural information helps users understand the page layout.
WCAG 4.1.2 — Name, Role, Value: For selectable lists, use role="listbox" on the container and role="option" on each item with aria-selected. For navigation lists, each item should contain an <a> or use role="link". The current page in a navigation list should have aria-current="page".
WCAG 2.1.1 — Keyboard: Interactive lists must be fully keyboard navigable. Two patterns exist:
tabindex="0", the rest are tabindex="-1". Arrow keys move focus between items. This keeps the list as a single tab stop.For selectable lists, Space selects/deselects the focused item. For navigation lists, Enter activates the link.
WCAG 2.4.7 — Focus Visible: Focused list items must display a clearly visible focus indicator. Ensure the focus ring has at least 3:1 contrast against adjacent colors (WCAG 1.4.11). Check with the Contrast Checker.
WCAG 1.4.3 — Contrast: Primary text must achieve 4.5:1 against the list background. Secondary text (often lighter/smaller) must also meet this threshold — a common failure point. Verify secondary text color with the Contrast Checker.
Virtualized lists: When using windowed/virtualized rendering (react-window, tanstack-virtual), ensure items outside the viewport are still announced correctly. Set aria-setsize and aria-posinset on each visible item so screen readers know the total list size and the current item's position.
Reorderable lists: Drag-and-drop reordering must have a keyboard alternative. Typically: select an item, press Space to "grab" it, use Arrow keys to move it, press Space to "drop." Announce position changes via an ARIA live region: "Item 3, moved to position 5 of 12."
Do:
<ul>, <ol>, <li>) as the foundationDon't:
<div> soup where semantic <ul>/<li> would workSpacing guidelines (use the Spacing Tool):
<!-- Basic unordered list -->
<ul class="list" role="list">
<li class="list__item">
<div class="list__leading">
<img src="avatar.jpg" alt="" class="avatar avatar--sm" />
</div>
<div class="list__content">
<span class="list__primary">Jane Cooper</span>
<span class="list__secondary">jane@example.com</span>
</div>
<div class="list__trailing">
<span class="badge badge--subtle badge--green">Active</span>
</div>
</li>
<li class="list__divider list__divider--inset" role="separator"></li>
<li class="list__item">
<div class="list__leading">
<img src="avatar2.jpg" alt="" class="avatar avatar--sm" />
</div>
<div class="list__content">
<span class="list__primary">Alex Johnson</span>
<span class="list__secondary">alex@example.com</span>
</div>
<div class="list__trailing">
<span class="badge badge--subtle badge--gray">Inactive</span>
</div>
</li>
</ul>
<!-- Navigation list -->
<nav aria-label="Settings">
<ul class="list list--interactive" role="list">
<li class="list__item" tabindex="0">
<div class="list__leading">
<svg aria-hidden="true"><!-- icon --></svg>
</div>
<div class="list__content">
<span class="list__primary">Account</span>
<span class="list__secondary">Manage your profile and preferences</span>
</div>
<div class="list__trailing">
<svg aria-hidden="true"><!-- chevron --></svg>
</div>
</li>
</ul>
</nav>
<style>
.list {
list-style: none;
margin: 0;
padding: var(--space-2) 0;
}
.list__item {
display: flex;
align-items: center;
padding: var(--space-3) var(--space-4);
gap: var(--space-3);
}
.list--interactive .list__item {
cursor: pointer;
transition: background var(--duration-fast);
}
.list--interactive .list__item:hover {
background: var(--color-surface-hover);
}
.list__content {
flex: 1;
min-width: 0;
}
.list__primary {
display: block;
font-size: var(--font-size-sm);
color: var(--color-text-primary);
}
.list__secondary {
display: block;
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list__divider--inset {
border-bottom: 1px solid var(--color-border-subtle);
margin-left: 56px; /* avatar (32px) + gap (12px) + padding (12px) */
}
</style>interface ListItemData {
id: string;
primaryText: string;
secondaryText?: string;
leading?: React.ReactNode;
trailing?: React.ReactNode;
disabled?: boolean;
href?: string;
}
interface ListProps {
items: ListItemData[];
variant?: 'simple' | 'two-line' | 'three-line';
divider?: 'full' | 'inset' | 'none';
interactive?: boolean;
dense?: boolean;
onItemClick?: (id: string) => void;
as?: 'ul' | 'ol' | 'nav';
}
function List({
items,
variant = 'simple',
divider = 'full',
interactive = false,
dense = false,
onItemClick,
as: Component = 'ul',
}: ListProps) {
const listClass = [
'list',
interactive && 'list--interactive',
dense && 'list--dense',
].filter(Boolean).join(' ');
const content = items.map((item, index) => (
<React.Fragment key={item.id}>
<li
className={`list__item${item.disabled ? ' list__item--disabled' : ''}`}
onClick={interactive && !item.disabled ? () => onItemClick?.(item.id) : undefined}
tabIndex={interactive && !item.disabled ? 0 : undefined}
aria-disabled={item.disabled || undefined}
role={interactive ? 'button' : undefined}
onKeyDown={interactive ? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onItemClick?.(item.id);
}
} : undefined}
>
{item.leading && <div className="list__leading">{item.leading}</div>}
<div className="list__content">
<span className="list__primary">{item.primaryText}</span>
{item.secondaryText && (
<span className="list__secondary">{item.secondaryText}</span>
)}
</div>
{item.trailing && <div className="list__trailing">{item.trailing}</div>}
</li>
{divider !== 'none' && index < items.length - 1 && (
<li
className={`list__divider list__divider--${divider}`}
role="separator"
/>
)}
</React.Fragment>
));
if (Component === 'nav') {
return (
<nav aria-label="List navigation">
<ul className={listClass} role="list">{content}</ul>
</nav>
);
}
return <Component className={listClass} role="list">{content}</Component>;
}Material Design 3 provides List, ListItem, ListItemButton, ListItemText, ListItemAvatar, ListItemIcon, ListItemSecondaryAction, ListSubheader, and Divider. MUI's composition model is rich — ListItemButton handles hover/focus/ripple for interactive items, while ListItemText accepts primary and secondary props for two-line rendering. The inset prop on Divider and ListItemText aligns content when some items lack icons. Material's list items use a 48px minimum height (56px with avatars) and follow the 8px spacing grid. Nested lists are achieved by placing a List inside a collapsible Collapse component within a ListItem.
Ant Design provides List with dataSource, renderItem, grid (for responsive grid layout), pagination, and loading (shows skeleton or spinner). Ant's List.Item supports actions (trailing action buttons) and List.Item.Meta for avatar + title + description layout. Ant's approach is data-driven — pass an array and a render function — making it ideal for API-fetched content. It includes built-in pagination and loading states.
Chakra UI does not provide a dedicated List component beyond HTML <List>, <ListItem>, <ListIcon>, and <OrderedList> / <UnorderedList>. These are thin wrappers around semantic HTML with Chakra's style props. For interactive/selectable lists, developers compose with Box, Flex, and Stack.
Radix UI does not provide a List primitive. Lists are considered an application-level composition of semantic HTML. For selectable lists, Radix's Select or Listbox pattern is the recommended approach.
Headless UI provides Listbox for selectable list behavior (keyboard navigation, ARIA roles) without any visual styling — the ideal starting point for custom selectable lists.
shadcn/ui does not ship a List component, but its documentation demonstrates list patterns using Tailwind utilities over semantic HTML. For command-palette style lists, shadcn's Command component (built on cmdk) is the go-to.
For consistent list spacing that aligns to your design system grid, use the Spacing Tool.