Designer view
List Item
A single row in a list — settings rows, contact lists, file browsers, transactions, message threads, search results. Distinct from Card (standalone object with no implicit siblings) by living inside an explicit list (`<ul>`, `<ol>`, or `<dl>`) and cooperating with sibling rows for keyboard navigation, selection semantics, and visual rhythm. Distinct from Tile (image-led, 2D grid) by being one-dimensional and text/metadata-led.
When to use
Use
For a one-dimensional list of related rows where each row carries primary identity plus optional supporting metadata — settings rows, contact lists, file browsers, message threads, search results, notification feeds. Rows cooperate via list semantics (sibling-aware keyboard navigation, selection, current-state).
Avoid
For standalone content surfaces with hierarchical composition — that is `Card`. For image-led 2D grid items — that is `Tile`. For table-like data with multiple sortable columns — that is `Table`. For floating selection from a fixed list — that is `Select`. For navigation between top-level pages with icons and sections — that is `SidebarNav`.
Versus related
- card
`Card` is a standalone object with no implicit siblings; `ListItem` lives inside an explicit list and cooperates with siblings. Visual treatment differs (Card is elevated/outlined surface; ListItem is dense row separated by dividers or spacing). Decision test: is each item independent (Card) or part of a sibling-aware collection (ListItem)?
- tile
`Tile` is image-led and arranges in a 2D grid; `ListItem` is text/metadata-led and arranges in a 1D list. ListItem suits dense metadata layouts; Tile suits image-led visual layouts.
- table
`Table` shows tabular data with sortable columns and column-aligned cells; `ListItem` is a free-form row with semantic slots (leading, primary, secondary, trailing). For data with multiple comparable attributes (price, date, sku, stock), use Table. For data where each row's content is heterogeneous or where visual variety matters per-row, use ListItem.
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | instance | List row component instance with leading + primary + trailing layout |
leading-icon | instance | Icon component instance; visibility per "has icon" property |
avatar | instance | Avatar component instance with image, initials, or icon fallback |
primary | text | Primary text style; truncates with ellipsis when single-line variant overflows |
secondary | text | Secondary text style; muted; visibility bound to two-line / three-line variant |
trailing-icon | instance | Icon component instance; visibility per "has trailing icon" property |
badge | instance | Badge component instance; visibility bound to "has badge" state |
action | instance | Icon button or button instance at the inline-end of the row |
Token usage per slot
root- spacing
- padding
spacing.compact - gap
spacing.compact
- padding
- radius
- corner
radius.sm
- corner
leading-icon- color
- foreground
color.text.muted
- foreground
avatar- radius
- corner
radius.full
- corner
primary- color
- foreground
color.text.primary
- foreground
- typography
- size
text.md - weight
weight.medium
- size
secondary- color
- foreground
color.text.muted
- foreground
- typography
- size
text.sm
- size
trailing-icon- color
- foreground
color.text.muted
- foreground
badge- spacing
- padding
spacing.tight
- padding
- radius
- corner
radius.pill
- corner
- color
- background
color.accent.bg - foreground
color.accent.fg
- background
- typography
- size
text.xs - weight
weight.semibold
- size
action- spacing
- padding
spacing.tight
- padding
- radius
- corner
radius.sm
- corner
- color
- ring
color.border.focus
- ring
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps standard / two-line / three-line. Drives layout — single-line is height-collapsed, two-line and three-line stack secondary text below primary. |
Density | Variant | density | comfortable / compact. |
Leading Type | Variant | leadingType | none / icon / avatar. Mutually exclusive — leading-icon and avatar share the same slot position. |
Interactive | Boolean | interactive | Whole row is clickable. Wraps row content in `<button>` or `<a>` activator. |
Selectable | Boolean | selectable | Whole row is selectable; visible leading checkbox appears. Drives `aria-selected` and `aria-checked`. |
Swipeable | Boolean | swipeable | Mobile swipe-to-reveal-actions enabled. Pairs with action slot for parity on desktop. |
Has Trailing Icon | Boolean | trailingIcon | — |
Has Badge | Boolean | badge | — |
Has Action | Boolean | action | — |
Primary | Text | primary | — |
Secondary | Text | secondary | — |
Avatar | Instance Swap | avatar | — |
Leading Icon | Instance Swap | leadingIcon | — |
Motion
| Transition | Duration token |
|---|---|
hoverState | motion.duration.fast |
swipeReveal | motion.duration.base |
selectToggle | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | At and below, density compact becomes the canonical default. Three-line variant collapses to two-line (third line moves into a "more details" disclosure). Swipeable variant becomes the dominant action-reveal pattern (replaces hover-reveal). Action slot may collapse into the swipe-revealed actions for the primary case. |
breakpoint.md | Above this width, variants render as authored. Hover-reveal patterns engage; swipe is mobile-only unless the consumer enables it explicitly. |
Internationalisation
RTL · mirroring
Leading position (icon / avatar) moves from inline- start (visual left in LTR) to inline-start (visual right in RTL). Trailing position (badge / trailing-icon / action) mirrors. Primary and secondary text inherit document direction. Swipe-to-reveal direction reverses: in LTR, swipe-left exposes the inline-end actions; in RTL, swipe-right exposes the same logical-end actions. ArrowKey navigation is direction-neutral (vertical lists; ArrowDown / Up same in both directions).
Text expansion
Primary text follows single-line truncation with ellipsis (full text via `aria-label` for SR fallback). Secondary text wraps on two-line and three-line variants. Long-text languages may force two-line variant where one-line was authored — the canonical guideline is to choose the variant per the longest-expected locale, not the average. Density compact risks crowding long-text labels; density comfortable is safer for international list layouts.
Variants, properties, states
Variants
Structurally different versions of the component.
standardtwo-linethree-line Properties
The same component, parameterised.
| Property | Type |
|---|---|
interactive | boolean |
selectable | boolean |
swipeable | boolean |
density | comfortable | compact |
leadingType | none | icon | avatar |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | idleselectedcurrent |
State transitions
| From | To | Trigger |
|---|---|---|
idle | selected | User toggles selection — clicking the leading checkbox (multi-select) or activating the whole row in single-select mode (`role="listbox"` parent). `aria-selected="true"` set; visual treatment changes. |
selected | idle | User toggles selection again or clears via programmatic update. |
idle | current | User navigates to the route owned by this row (for ListItem-as-nav usage). `aria-current="page"` set; visual treatment changes. Other rows in the same list lose their current state. |
current | idle | User navigates away. The row loses `aria-current="page"`. |
Figma↔Code mismatches
- 01 Figma
ListItem drawn as a Card-like surface
CodeA `<li>` row inside a list, not a standalone Card surface
ConsequenceDesigners may compose ListItem mocks with Card-like treatment (rounded corners, shadow, elevated surface). Implementations following the design ship rows that look like cards but live inside lists with dense vertical rhythm — visual hierarchy is confused, the list feels noisy, and the canonical Card-vs-ListItem distinction is lost.
CorrectListItem is a row in a list — visual treatment is restrained, separation between rows is via dividers or negative space, not via independent card-like surfaces. For Card-like surfaces in a layout, use Card or Tile directly; do not style ListItem as Card.
- 02 Figma
Whole-row click affordance + nested action button
CodeRow click + action button click both work, with action's `event.stopPropagation`
ConsequenceDesigners compose mocks with both a whole-row click target AND an inline action button. Implementations may forget the `stopPropagation` — clicking the action activates both the action AND the row's click handler; users get unexpected double-effects (delete a message AND navigate to it). Or developers may remove the action button entirely to avoid the conflict.
CorrectBoth whole-row click and action button are valid simultaneously — the action's onClick must call `event.stopPropagation()` to prevent row activation. Document the canonical pattern; the action is a separate target in the row's tab order, distinct from the row activation.
- 03 Figma
Selected state shown only by background colour
CodeSelected state shown by background + leading checkbox checked + `aria-selected="true"`
ConsequenceDesigners use a single visual change for selection. Implementations following the design ship colour-only selection, fail WCAG 1.4.1, and SR users hear no selection cue. The leading checkbox is omitted; users may not realise rows are selectable.
CorrectTriple-layer selection: visual (background + accent strip), accessible state (`aria-selected="true"` on row, `aria-checked` on checkbox), and visible checkbox (the canonical multi-select affordance for ListItem). For single-select listbox-pattern rows, the visible checkbox may be omitted in favour of visual-state-only-plus-aria-selected.
- 04 Figma
Inline links in secondary text
CodeInline interactive elements move to the action slot or break the row activation
ConsequenceDesigners compose secondary text with embedded inline links ("View profile, Edit, Delete"). Implementations following the design either ship the embedded links (which break whole-row click — clicking a link does not activate the row) or hoist them out (the design and production diverge).
CorrectInline interactive elements in row content are an antipattern. Move them to the action slot as a single action button (typically more-options menu) or split the row into multiple list items. Document this as mistake `listitem-secondary-as-link`.
Common mistakes
#listitem-not-in-list
ListItem used standalone outside a list
The component is rendered without a `<ul>` / `<ol>` parent. SR users hear the content but lose the list structure — no "list of N items" announcement, no list-item position cue. Standalone usage breaks the semantic contract.
Always render ListItem inside a real list. For single-row use cases (just one row), reconsider whether the canonical pattern is ListItem or some other component (a section header, a Card, a settings row grouped as part of a fieldset).
#listitem-action-no-stop-propagation
Action button click activates parent row
The whole row is clickable; the action button is nested inside. Action click bubbles to the row's handler; users invoking the action get the row's activation as a side effect.
Action button's onClick calls `event.stopPropagation()` to prevent bubbling. Document the pattern in the action slot's a11y hint and in the canonical reference.
#listitem-no-aria-selected
Selectable rows missing `aria-selected`
Visual selected state is shown but `aria-selected` is missing. SR users hear nothing about selection state. Or `aria-selected` is on the wrong element (the checkbox, not the row).
For listbox-pattern selection, `aria-selected` is on the row (`<li>`). For multi-select with checkbox, `aria-checked` is on the checkbox AND `aria-selected` is on the row — both communicate selection redundantly. Pair with visible visual treatment.
#listitem-swipe-no-keyboard
Mobile swipe-to-delete with no keyboard equivalent
Swipeable rows expose actions on mobile via swipe gestures. Keyboard users on desktop cannot perform the swipe-revealed actions; they remain hidden.
Every swipe-revealed action also reachable via Tab and Enter. Common pattern: the action button is always in the DOM but visually hidden (or visible on hover) on desktop; swipe reveals the same buttons on mobile. Both input modalities have access.
#listitem-no-roving-tabindex
Selectable list with every row tabindex="0"
Tab cycles through every row in a selectable list (could be 100+ rows). APG listbox / grid pattern uses roving tabindex — only one row is in the tab order, ArrowKeys navigate within.
Roving tabindex per APG listbox or grid pattern. Only the currently-focused row has `tabindex="0"`; others have `tabindex="-1"`. ArrowKeys move focus within; Tab moves focus out of the list to the next document focusable.
#listitem-secondary-text-as-link
Inline links embedded in secondary text
Secondary text contains "View · Edit · Delete" as inline links. The whole-row click handler conflicts with link clicks; or the action slot becomes redundant; or developers omit the action slot entirely and rely on embedded links (breaking keyboard reachability).
Inline interactions move to the action slot as a single action button (typically a more-options MenuButton). Secondary text remains plain prose. Document the pattern; reject inline-link mocks at design review.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
root | Always wrap inside a real list (`<ul>` / `<ol>`) for the canonical list semantic. For interactive variants, the row may be wrapped in a `<button>` or `<a>` (whole- row clickable). For selectable variants, the row carries `aria-selected` and lives inside `role="listbox"` (single-select) or has a leading checkbox (multi-select). For current-page variants (in navigation lists), the row carries `aria-current`. | |
leading-icon | Decorative when paired with a primary text label (`aria-hidden="true"`). For information-carrying icons (a verified-account checkmark, a syncing-status indicator), surface the meaning via visually-hidden text composed into the row's accessible name. | |
avatar | For person avatars, alt text is the person's name (often redundant with the primary label — set `alt=""` then, with the avatar as decorative). For status-indicating avatars (presence dots overlaid on the avatar), surface status via visually-hidden text in the row's accessible name ("Alice (online)"). | |
primary | Plain text. The row's primary identifier for SR users. Avoid visually-hidden modifiers in the primary slot — the visible text and the accessible name should match. | |
secondary | Plain text. SR users hear primary then secondary in DOM order when navigating into the row. Avoid embedding inline interactive elements (links, buttons) in secondary text — they break the row's whole-row-clickable contract; put them in the action slot instead. | |
trailing-icon | Decorative when communicating affordance (chevron signalling "tap for more"); status-bearing when carrying meaning not in the primary text (a "syncing" indicator). For status, surface via visually-hidden text in the row's accessible name. | |
badge | For numeric badges, append the count to the row's accessible name via visually-hidden text ("Inbox, 3 unread"). Decorative-only badges (a pulse for "new") use `aria-hidden="true"` with the meaning conveyed in primary text or via composed accessible name. | |
action | Real `<button>` with accessible name. For whole-row-clickable rows, the action button's click must `event.stopPropagation()` to prevent activating the parent row. The action button is reachable via Tab; for selectable list rows, ArrowKeys do NOT navigate to the action — only Tab does. |