Components · Layout and structure

List

A vertical sequence of rows that share a slot grammar — leading, content, trailing. Carries activity feeds, settings panels, selectable choices, and navigation menus. The most common dense surface in the product.

Documentedby Derek Fidler

Today

Bank account

Transfer sent

€ -200,00

Settlement account

Transfer received

€ 200,00

Vendor name

Payment sent

€ -200,00

Customer name

Payment received

€ 200,00


Yesterday

Vendor name

Pending payment

€ -200,00

Vendor name

Pending card payment

€ -200,00

Vendor name

Card payment

€ -200,00

Vendor name

Pending online payment

€ -200,00


16 Apr 2026

Bank account

Transfer sent

€ -200,00

Settlement account

Transfer received

€ 200,00

Vendor name

Denied online payment

€ -200,00

Vendor name

Online payment

€ -200,00

Overview

A list is a column of rows that share a structure. Every row reads as a tuple of three slots: a leading visual (avatar, icon, radio), a content column (title, optionally supporting text), and a trailing cell (a metric, a chevron, a toggle, or nothing). The slots are the contract; what fills them changes per surface.

When in doubt, reach for a list

Lists are the most under-used and most fitting layout for product data. Tables earn their place when columns need to be compared across rows; card grids earn theirs when each row has equal weight and rich media. Almost everything else is a list.

Anatomy

Five named parts inside the row, plus the section header that groups rows above it. The slots are optional in pairs — leading + trailing can both be absent — but the title is required. Without a title the row has nothing to read.

Today

Bank account

Transfer sent

€ -200,00

  1. Leading slot

    Avatar (40 px), icon (24 px), check, radio, thumbnail, or nothing. Anchors the row identity at a glance.

  2. Title

    Inter Tight Medium, 16 / 24. Required. The thing the row is about — a name, a label, an action.

  3. Supporting text

    Inter Tight Regular, 14 / 20, in text.secondary. One line. Status, timestamp, role, or other context.

  4. Trailing slot

    Currency, chevron, toggle, badge, or nothing. Right-aligned. Metric uses tabular numerals so columns stack.

  5. Section header (optional)

    Above the slot. Groups rows by date, status, or category. Inter Tight Semibold 14 / 20, text.secondary.

Content

Three content shapes cover almost every surface. Pick the lightest shape that carries the meaning — single-line where the metric and label work together, two-line where the row needs context, two-line + chevron when the row leads somewhere.

Single-line

Bank account

€ -200,00

Title only. Pairs with compact density. Use when the metric and label carry the meaning together.

Two-line

Bank account

Transfer sent

€ -200,00

The default. Title + one supporting line. Covers most product surfaces.

Two-line + chevron

Bank account

Transfer sent · Today, 14:32

Navigation list. The chevron tells the user the row leads somewhere. No trailing metric.

Density

Three densities tied to typical content. Pick deliberately — density is a design decision, not a styling one. The same component at different densities tells the user something different about the page.

compact · 32 px

Bank account

€ -200,00

Settlement account

€ 200,00

Title only, 32 px tall. Dense surfaces — recent activity dropdowns, tooltips.

default · 44 px

Bank account

Transfer sent

€ -200,00

Settlement account

Transfer received

€ 200,00

Title + supporting, 44 px tall. The standard product list.

comfortable · 56 px

Bank account

Transfer sent

€ -200,00

Settlement account

Transfer received

€ 200,00

Title + supporting, 56 px tall, larger leading. Marketing surfaces, generous detail pages.

States

Six visible states for interactive lists. Read-only lists (an activity feed, a receipt) sit at default and never react to hover. Promote a list to interactive only when the row leads somewhere or can be selected.

  • Default

    Bank account

    Transfer sent

    € -200,00

  • Hover

    Bank account

    Transfer sent

    € -200,00

  • Pressed

    Bank account

    Transfer sent

    € -200,00

  • Selected

    Bank account

    Transfer sent

    € -200,00

  • Focus

    Bank account

    Transfer sent

    € -200,00

  • Disabled

    Bank account

    Transfer sent

    € -200,00

Hover and pressed are background shifts only

No scale transform, no shadow, no glow. The state difference is read by the eye via tone, not motion. background.accent.neutral.subtler for hover, .subtle for pressed, accent.blurple.subtlest for selected.

Selection

Two selection patterns. Single-select uses a leading radio mark; multi-select uses a leading checkbox. The selected row inherits the discovery accent — blurple subtlest — so the eye can find it on a long list.

Single-select · radio

One row at a time. Use role="radiogroup".

Bank account

ending 4242

Card on file

ending 0102

Pay by transfer

settles in 1–2 days

Multi-select · checkbox

Any row can be on or off. Use role="group" and standard checkbox semantics.

Demo-ready

ready to share with leadership

Building

actively in development

Archived

kept for reference

Sections and dividers

A section is a labelled group of rows: a date header (Today, Yesterday, the absolute date for older), a status group (Demo-ready, Building, Archived), or a category. Sections are separated by a 1 px divider with 16 px padding above and below. Group headers sit at text.secondary so they read as metadata, not content.

Don't sticky every section header

Sticky headers are the right call for a long, scrolled feed where the user needs to know where they are (a transfers list across months). They're overkill for a settings panel or a short-bounded list. When in doubt, leave them static.

Empty and loading

Empty states teach the system: name what shows up here, and how the first row arrives. Loading states mirror the row's shape — a pulsing avatar, two lines of content, a trailing pill — so the layout never reflows when data lands.

Empty

No transfers yet.

Settled payments and outgoing transfers land here once your first payout clears.

Title in the row's normal weight; supporting copy explains what arrives. Don't illustrate.

Loading · skeleton

Skeleton mirrors the row's shape — leading circle, two lines of content, trailing pill. Same width buckets across rows.

Behavior

  • Read-only by default. Most lists in the product (activity feeds, receipts, exported data) don't respond to hover. Promote a list to interactive only when the row navigates or can be selected.
  • Whole row is the click target. When interactive, the entire row activates — not just the title. Minimum 44 px height for any interactive row to meet touch-target guidance.
  • Trailing actions are separate. If a row needs both a primary navigation and a secondary action (kebab menu, delete), the secondary action is a button inside the trailing slot — it stops propagation so a click on the icon doesn't fire the row's navigation.
  • Truncate, don't wrap. Title and supporting text both truncate with ellipsis. The row keeps a fixed height; long content falls off the right edge rather than expanding the row.
  • Trailing metrics align. Currency, percentages, and counts use tabular-nums slashed-zero so the decimal point stacks down a column without column shimmer.

Accessibility

  • Read-only list: use semantic <ul> / <li> elements. Screen readers announce the count and let the user step through with arrow keys.
  • Navigation list: wrap each row in an <a> or <button>. The row inherits keyboard focus and Enter activation natively.
  • Single-select: role="radiogroup" on the list, role="radio" + aria-checked on each row. Keyboard nav: arrow keys move and select, Tab leaves the group.
  • Multi-select: standard checkbox semantics. Each row carries its own checked state; Tab moves between rows.
  • Section headers use <h3> / <h4>at the page's appropriate level so screen readers can use them as landmarks.
  • Roving tabindex for long interactive lists — one row carries tabindex=0, others -1. Arrow keys move the focus mark; Tab leaves the list. Skips the “tab through 300 rows” problem.
  • Status carried by colour pairs with copy— the failed avatar dot is decorative; the supporting text says “Denied online payment”. A screen-reader user without colour loses nothing.

Code

The List composes from List, ListSection, and ListItem primitives in @flatpay-dk/ui. Slots are plain children — pass any node into leading / trailing.

tsx

import { List, ListSection, ListItem, Avatar } from "@flatpay-dk/ui";
import { NorthEastOutlined, SouthWestOutlined } from "@mui/icons-material";

// Read-only feed
<List>
  <ListSection label="Today">
    <ListItem
      leading={<Avatar><NorthEastOutlined /></Avatar>}
      title="Bank account"
      supporting="Transfer sent"
      trailing="€ -200,00"
      trailingTone="negative"
    />
    <ListItem
      leading={<Avatar><SouthWestOutlined /></Avatar>}
      title="Settlement account"
      supporting="Transfer received"
      trailing="€ 200,00"
      trailingTone="positive"
    />
  </ListSection>
</List>

// Navigation list
<List>
  <ListItem
    as="a"
    href="/transfers/abc"
    leading={<Avatar>…</Avatar>}
    title="Bank account"
    supporting="Today, 14:32"
    chevron
  />
</List>

// Single-select
<List role="radiogroup" aria-label="Payment method">
  <ListItem
    role="radio"
    aria-checked={method === "bank"}
    onClick={() => setMethod("bank")}
    leading={<Radio checked={method === "bank"} />}
    title="Bank account"
    supporting="ending 4242"
  />
</List>

Best practices

Lists are the workhorse layout. Small habits compound across screens.

Today

Bank account

€ -200,00

Settlement

€ 200,00

Do

Group by date or status when a feed spans more than ~10 rows. Sections are how the user finds what they came for.

Bank account

€ -200,00

Settlement

€ 200,00

Page 1 of 12 · ‹ ›

Don't

Don't paginate before grouping. A 50-row paginated list with no headers is harder to scan than 50 grouped rows on one page.

Bank account

Transfer sent

€ -200,00

Do

Title is the noun, supporting is the context. 'Bank account' / 'Transfer sent' reads at a glance.

Transfer sent to bank

14:32

€ -200,00

Don't

Don't bury the noun. 'Transfer sent to bank account' as the title means the eye reads four words to get to the same answer.

Bank account

€ -200,00

Settlement

€ 200,00

Vendor name

€ -200,00

Do

Negatives in orange, positives in green. Voided is line-through and muted — three colour roles, three meanings.

Bank account

Transfer sent

€ -200,00

imagine this row in red — it reads as an error, not a transfer

Don't

Don't paint negatives in red. Outgoing money is normal product motion; red is reserved for errors and destructive states.

Props

Two primitives ship: the List container and the ListItem row. Section headers are a thin wrapper component.

List

PropTypeDefaultDescription
density"compact" | "default" | "comfortable""default"Sets the row height token for every ListItem inside. compact=32 px, default=44 px, comfortable=56 px.
interactivebooleanfalseWhen true, rows render hover/pressed states and become focusable. Use when each row navigates or selects.
role"list" | "listbox" | "radiogroup" | "menu" | "group""list"ARIA role for the wrapping element. Choose based on whether the list is read-only, selectable, or actionable.
childrenReactNodeListSection or ListItem nodes. Sections render a label + slot; items render a row.

ListItem

PropTypeDefaultDescription
titlestringThe row's primary text. Required. Inter Tight Medium 16 / 24, truncates with ellipsis.
supportingstringOptional second line. Inter Tight Regular 14 / 20 in text.secondary. Status, timestamp, role, etc.
leadingReactNodeAvatar, icon, radio, checkbox, thumbnail, or any 32–56 px element. Sits before the content column.
trailingReactNodeCurrency, badge, toggle, count, or any right-aligned element. Use tabular-nums for metrics so columns stack.
trailingTone"primary" | "positive" | "negative" | "voided" | "secondary""primary"Colour role for the trailing slot. positive=green.800, negative=orange.800, voided=line-through+muted.
chevronbooleanfalseRenders a right-chevron at the far end. Implies the row navigates somewhere.
as"div" | "li" | "a" | "button""li"Element rendered. Use "a" or "button" for interactive rows; the row's content becomes the click target.
...restHTMLAttributesAll standard HTML attributes pass through (onClick, href, aria-*, etc.).