Skip to content

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

All triggers

Specification body

Spec — Lists

Facet Visual of Koder Design. Material parity: https://m3.material.io/components/lists.

3 row densities

DensityHeightUse
Single-line56 pxCompact list; just labels
Two-line72 pxLabel + supporting text
Three-line88 pxLabel + 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

TypeSizeUse
Icon24 pxSettings entries, semantic action label
Avatar40 pxPeople / accounts
Image56 × 56 pxFiles, products, content thumbnails
Checkbox18 pxMulti-select list
Radio18 pxSingle-select list
None0 (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

TypeUse
Icon button (24 px)Overflow menu, secondary action
SwitchToggle per row (Settings)
Metadata textRight-aligned label (file size, count, date)
Chevron (›)Indicates row navigates somewhere
EmptyRow 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 targetAction
Whole rowPrimary action (navigate, open, expand)
Leading checkbox / radioToggle selection (whole row also toggles)
Trailing icon buttonSecondary action (overflow, settings)
Trailing switchToggle 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

StateVisual
RestBase
HoverState layer 8% over row
PressedState layer 12%
Focused2 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
Disabled38% 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 useWhen 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-selected on selected
  • Trailing icon buttons: aria-label describing 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)

DensitySingleTwo-lineThree-line
Compact48 px64 px80 px
Default56 px72 px88 px
Comfortable64 px84 px96 px

Inherits from customization.kmd.

R13 — Per-preset variation

PresetRow styleSelected style
material3Flat tonal, 0 px cornerssecondary-container bg
material2Flat, no roundingFilled tonal bg
ios_cupertinoHairline separators, indented insetFilled blue bg
gnomeAdwaita boxed-list rows with 12 px radiusAccent tint
windows_11Compact, hover bandAccent bar on left edge
brutalistSolid borders between every rowInverted colors
terminal_classicSingle-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
  • interaction/selection.kmd — multi-select / single-select patterns
  • interaction/states.kmd — row state layers
  • themes/color-roles.kmdsurface / secondary-container / outline-variant
  • themes/typography.kmdbody-large / body-medium
  • components/dividers.kmd — inset divider style
  • components/checkbox.kmd — multi-select leading
  • components/switch.kmd — trailing toggle pattern
  • foundations/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-container tonal 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