Overview
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:
- To present a scrollable collection of similar items (emails, contacts, files, messages)
- To display settings or configuration options with descriptions
- To show navigation links in a sidebar or menu
- To render search results with heterogeneous content
When NOT to use a List:
- For tabular data with multiple sortable columns — use a Table
- For hierarchical nested data — use a Tree View
- For card-based browsing layouts — use a Card grid
- For key-value pairs — use a description 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.
Variants
List Variants
| 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. |
Item Slot Variants
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 |
Divider Styles
| 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).
Properties
List Properties
| 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 |
ListItem Properties
| 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 |
Token Mappings
Design Token Mappings
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.
States
List States
| 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. |
Accessibility
Accessibility
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:
- Roving tabindex: One item in the list is
tabindex="0", the rest aretabindex="-1". Arrow keys move focus between items. This keeps the list as a single tab stop. - Tab per item: Each item is individually tabbable. Simpler but creates many tab stops for long lists.
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."
Usage Guidelines
Usage Guidelines
Do:
- Use semantic list elements (
<ul>,<ol>,<li>) as the foundation - Keep list items consistent — every item should have the same slot layout within a single list
- Use inset dividers when items have leading content (icons, avatars) to create visual alignment
- Implement virtualization for lists exceeding ~100 items to maintain scroll performance
- Provide an empty state message when the list has no items
Don't:
- Don't mix wildly different item layouts within the same list — this confuses users and screen readers
- Don't use lists for tabular data that needs column headers and sorting — use a Table
- Don't remove all visual separators between items unless spacing alone creates clear boundaries
- Don't auto-focus the first list item on page load unless the list is the primary interaction target
- Don't use
<div>soup where semantic<ul>/<li>would work
Spacing guidelines (use the Spacing Tool):
- Standard item padding: 12–16px vertical, 16px horizontal
- Dense item padding: 8px vertical, 16px horizontal
- Gap between leading slot and content: 12–16px
- Gap between content and trailing slot: 8–12px
- Section header padding: 8px vertical, 16px horizontal
Code Snippets
<!-- 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>;
}Design Systems
Design System Implementations
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.