Lists
components specs/components/lists.kmd
Vertical stack of related items, each row showing a label and optional leading / trailing elements. Material parity (`/components/lists`). Covers single-line, two-line, three-line rows; leading elements (icon, avatar, image, checkbox); trailing elements (icon, switch, metadata); and selection / nav behavior.
When this spec applies
Primary triggers
- Add a list
All triggers
- Show a vertical collection of items with labels
- Build a settings screen
- Display a contact / file / message list
Specification body
Spec — Lists
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/lists.
3 row densities
| Density | Height | Use |
|---|---|---|
| Single-line | 56 px | Compact list; just labels |
| Two-line | 72 px | Label + supporting text |
| Three-line | 88 px | Label + 2 lines supporting text |
Pick by content. Don't mix densities within a single list — looks ragged.
Anatomy (two-line row with leading + trailing)
┌────────────────────────────────────────────────────┐
│ 👤 Jamie Garcia ⋮ │
│ Engineering · last seen 5m ago │
└────────────────────────────────────────────────────┘
↑ ↑ ↑
leading primary text trailing
+ supporting text
- Row height: 72 px (two-line)
- Padding: 16 px horizontal
- Leading element: 24 px icon / 40 px avatar / 56 px image; 16 px gap to text
- Primary text:
body-large(16/24, weight 400) - Supporting text:
body-medium(14/20, weight 400),text-muted - Trailing: 24 px icon button OR text metadata
- Container bg:
surface(transparent / parent surface)
R1 — Leading element types
| Type | Size | Use |
|---|---|---|
| Icon | 24 px | Settings entries, semantic action label |
| Avatar | 40 px | People / accounts |
| Image | 56 × 56 px | Files, products, content thumbnails |
| Checkbox | 18 px | Multi-select list |
| Radio | 18 px | Single-select list |
| None | 0 (text starts at 16 px) | Compact text lists |
Within a single list, ONE leading element type — don't mix avatar rows with icon rows in the same list.
R2 — Trailing element types
| Type | Use |
|---|---|
| Icon button (24 px) | Overflow menu, secondary action |
| Switch | Toggle per row (Settings) |
| Metadata text | Right-aligned label (file size, count, date) |
| Chevron (›) | Indicates row navigates somewhere |
| Empty | Row is a single-purpose row (just label + leading) |
Maximum 1 trailing element per row. If multiple actions needed, collapse into overflow menu.
R3 — Row interaction
| Tap target | Action |
|---|---|
| Whole row | Primary action (navigate, open, expand) |
| Leading checkbox / radio | Toggle selection (whole row also toggles) |
| Trailing icon button | Secondary action (overflow, settings) |
| Trailing switch | Toggle setting (does NOT trigger row's primary action) |
When trailing has a switch, the row's primary action is the switch toggle — the entire row tap also flips the switch (and announces the change).
R4 — States
| State | Visual |
|---|---|
| Rest | Base |
| Hover | State layer 8% over row |
| Pressed | State layer 12% |
| Focused | 2 px focus ring on row edge OR on focused element |
| Selected (single-select) | secondary-container tonal bg |
| Selected (multi-select) | Checkbox checked + state layer 12% bg |
| Disabled | 38% opacity on text + leading + trailing |
R5 — Multi-line text rules
- Primary text: 1 line max, ellipsis on overflow
- Supporting text: 1 line (two-line) or 2 lines (three-line); ellipsis on overflow
- Don't allow primary text to wrap to multiple lines — escalate density tier (single → two → three) before allowing wrap
- For arbitrary-length content (chat preview, description), use a card instead
R6 — Subheaders inside a list
Use subheader divider (see components/dividers.kmd § R5) to group
sections within a list:
Recent
────────────────────────────────────
👤 Jamie Garcia ›
👤 Pat Wong ›
👤 Riley Lee ›
All contacts
────────────────────────────────────
👤 Alex Brown ›
👤 Chris Wong ›
Subheader text is label-large, text-muted, 16 px left padding,
12 px top padding.
R7 — Dividers between rows
| When to use | When to skip |
|---|---|
| > 5 rows with same density | ≤ 5 rows |
| Mixed-content rows (some have trailing, some don't) | Uniform rows |
| Settings screen (helps scan) | Card-like list (cards already separated) |
Divider style is inset by default (aligned with text column, not
the leading element column) — see dividers.kmd § R3.
R8 — Sticky list section headers
For long lists with many sections (contacts, files), subheaders can stick to the top of the viewport as the user scrolls:
- Sticky behavior: when the section's first row scrolls off-screen, the subheader sticks to the viewport top until the next section arrives, then it transitions to the new section's subheader
- Use sparingly — when the list is long enough that scroll position loses context
R9 — Empty state
When list is empty:
- Show illustration / icon (48-64 px)
- Show heading + brief description
- Optional CTA button ("Add your first contact")
- Vertically centered in available space
Don't show a literal empty list row — disambiguates from loading.
R10 — Loading state
Skeleton rows matching the actual row anatomy:
- Same height per row
- Leading element as gray rectangle / circle (matching shape)
- Primary text as ~60% width gray bar
- Supporting text as ~40% width gray bar
- 6-8 skeleton rows by default
Fade-in real content as it loads.
R11 — Accessibility
- List container:
role="list"(HTML<ul>semantically) - Each row:
role="listitem" - Selection list:
role="listbox"+role="option"per row +aria-selectedon selected - Trailing icon buttons:
aria-labeldescribing action - Trailing switches:
role="switch"+aria-checked - Row that navigates: native link semantics (
<a href>wrapping row content) - Keyboard:
- Arrow Up / Down: moves focus between rows
- Enter / Space: activates row's primary action
- Tab: enters list → next focusable after list
- Home / End: first / last row
R12 — Density (rows)
| Density | Single | Two-line | Three-line |
|---|---|---|---|
| Compact | 48 px | 64 px | 80 px |
| Default | 56 px | 72 px | 88 px |
| Comfortable | 64 px | 84 px | 96 px |
Inherits from customization.kmd.
R13 — Per-preset variation
| Preset | Row style | Selected style |
|---|---|---|
material3 | Flat tonal, 0 px corners | secondary-container bg |
material2 | Flat, no rounding | Filled tonal bg |
ios_cupertino | Hairline separators, indented inset | Filled blue bg |
gnome | Adwaita boxed-list rows with 12 px radius | Accent tint |
windows_11 | Compact, hover band | Accent bar on left edge |
brutalist | Solid borders between every row | Inverted colors |
terminal_classic | Single-line > item name [trailing] | Asterisk prefix * item |
R14 — Forbidden patterns
- ❌ Mixed density within a list (single + two-line rows interleaved)
- ❌ Mixed leading element types (avatar + icon rows mixed)
- ❌ More than 1 trailing element per row
- ❌ Long-form text wrapping to many lines (use card instead)
- ❌ Tap-target hit smaller than 48 × 48 px on a row
- ❌ Trailing switch where tapping row navigates elsewhere (conflicting actions)
- ❌ Selected row without strong-enough contrast (rely on color + border OR fill + check)
- ❌ Loading state that flashes content before settling
Cross-link
interaction/selection.kmd— multi-select / single-select patternsinteraction/states.kmd— row state layersthemes/color-roles.kmd—surface/secondary-container/outline-variantthemes/typography.kmd—body-large/body-mediumcomponents/dividers.kmd— inset divider stylecomponents/checkbox.kmd— multi-select leadingcomponents/switch.kmd— trailing toggle patternfoundations/elements.kmd— Container + Control families
Requirements (testable)
Requirement: Row density selection and consistency {#req-lists-density}
A list SHALL render every row at exactly one of the three defined density tiers (single-line 56 px, two-line 72 px, three-line 88 px at the Default scale) and SHALL NOT interleave rows of different density tiers within the same list.
Scenario: Two-line rows render at the tier height
- GIVEN a list configured as two-line density at the Default scale
- WHEN the list renders a row with a primary label and one supporting line
- THEN the row height is 72 px
- AND horizontal padding is 16 px on each side
Scenario: Mixing densities is rejected
- GIVEN a list whose declared density is single-line
- WHEN a row supplying two lines of supporting text is added to that list
- THEN the list is flagged as an invalid mixed-density list
- AND the row is not rendered at a taller density than its siblings
Requirement: Single leading element type per list {#req-lists-leading}
Within one list every row SHALL use the same leading element type (icon 24 px, avatar 40 px, image 56 px, checkbox 18 px, radio 18 px, or none), and a list SHALL NOT mix leading element types across its rows.
Scenario: Avatar list keeps a uniform leading type
- GIVEN a contacts list whose leading element type is avatar
- WHEN each row renders its leading element
- THEN every row shows a 40 px avatar with a 16 px gap to the text column
Scenario: Icon row in an avatar list is rejected
- GIVEN a list whose leading element type is avatar
- WHEN a row requests a 24 px icon as its leading element
- THEN the list is flagged as an invalid mixed-leading list
Requirement: At most one trailing element per row {#req-lists-trailing}
A row SHALL carry at most one trailing element (icon button, switch, metadata text, or chevron); when more than one trailing action is needed the additional actions SHALL be collapsed into an overflow menu.
Scenario: A second trailing action collapses to overflow
- GIVEN a row that already has a trailing metadata text element
- WHEN a second trailing action is requested for the same row
- THEN the row does not render two trailing elements side by side
- AND the additional actions are exposed through a single overflow icon button
Requirement: Trailing switch drives the row primary action {#req-lists-switch}
When a row's trailing element is a switch, tapping anywhere on the row SHALL toggle that switch and announce the change, and the row tap SHALL NOT trigger any navigation or other primary action.
Scenario: Row tap flips the trailing switch
- GIVEN a settings row whose trailing element is a switch currently in the off state
- WHEN the user taps the row body outside the switch
- THEN the switch moves to the on state
- AND the change is announced to assistive technology
- AND no navigation away from the settings screen occurs
Requirement: Selection and disabled state rendering {#req-lists-states}
A selected row SHALL render a distinct state (single-select: secondary-container tonal background; multi-select: checked checkbox plus a 12% state-layer background), and a disabled row SHALL render its text, leading, and trailing content at 38% opacity.
Scenario: Single-select row shows tonal selection
- GIVEN a single-select list with no row selected
- WHEN the user selects a row
- THEN that row's background becomes the
secondary-containertonal color - AND the previously selected row, if any, returns to the rest background
Scenario: Disabled row is dimmed
- GIVEN a list row marked disabled
- WHEN the row renders
- THEN its text, leading element, and trailing element are drawn at 38% opacity
- AND tapping the row triggers no primary action
Requirement: Accessibility roles and keyboard navigation {#req-lists-a11y}
A list SHALL expose list semantics (role="list" with role="listitem" rows, or role="listbox"/role="option" with aria-selected for selection lists), and SHALL support keyboard navigation: Arrow Up/Down moves focus between rows, Enter/Space activates the focused row's primary action, and Home/End moves focus to the first/last row.
Scenario: Selection list exposes listbox semantics
- GIVEN a single-select list with one row selected
- WHEN the accessibility tree is inspected
- THEN the container exposes
role="listbox" - AND each row exposes
role="option" - AND the selected row carries
aria-selected="true"
Scenario: Arrow Down moves focus to the next row
- GIVEN keyboard focus on the first row of a list
- WHEN the user presses Arrow Down
- THEN focus moves to the second row
Scenario: End moves focus to the last row
- GIVEN keyboard focus on the first row of a list
- WHEN the user presses End
- THEN focus moves to the last row
Scenario: Enter activates the focused row
- GIVEN keyboard focus on a navigable row
- WHEN the user presses Enter
- THEN the row's primary action is activated
References
specs/foundations/elements.kmdspecs/themes/color-roles.kmdspecs/themes/typography.kmdspecs/interaction/selection.kmdspecs/interaction/states.kmdspecs/components/dividers.kmd