Records list
Reach for this as the compact-class counterpart of the table — the same collection below the table breakpoint, one primary line per record under the same narrowing chrome. A verb on this screen has three homes and none of them is the bar: the primary one is the Fab (fab), view verbs like Export are the view-options surface's header affordance, and a record's own actions belong to the pushed record surface — see Detail's hosting knob.
Spec of record: design-system/components/data/List.d.ts
Live
Real widgets, built from the real kit. Turn a knob and it rebuilds; the theme bar above applies the design system's own tokens — including anything you changed under Customize — and the device selector uses the same sizes the golden baselines gate on, so what you see here is what CI holds the widget to.
Press Focus on any preview to take it to the full window — at a desktop width that renders most devices at 1:1, which is when the widget's own controls become clickable. The bar always states the device and the scale, so a shrunk preview says it is shrunk. Esc leaves.
Set to 0 for the empty state.
Which nav the compact posture mounts — the drawer behind the hamburger, or a bottom bar.
The primary tier, and on a records destination the ONLY loud action the bar's neighborhood has. The scaffold renders it in the thumb zone at every window class; a screen never places it itself.
Actions in the drawer's own footer. The drawer carries them at EVERY window class — at 960 and wider it exists only because they do, since the expanded shell otherwise mounts a standing inline rail instead.
Shows when you open the drawer — the hamburger below 960, an edge swipe at 960 and wider, where the rail head's menu button toggles the rail instead. The actions sit in the drawer's own footer; the resting frame is identical either way. — press Focus to make the preview interactive.
The app's identity mark, and nothing else — it never opens a picker. At 960 and wider it sits beside the menu button in the rail's head at the same place in both postures, overhanging the collapsed rail so the bar beside it steps in. Below 960 the drawer's header band states it.
The scope — one value of the app's own kind (here an Area) — as the FIRST item of the bar's subtitle, ahead of the meta. With a picker it is a quiet text button with a chevron; on a phone the whole title block opens the picker as a bottom sheet. A pushed record's bar shows it read-only.
Global commands with no page ("Send feedback", "Help") pinned in the rail's footer under the footer destinations — the same row as a destination, but never lit and never counted in the destination index.
An organization's or partner's logo at the very bottom of the rail and the drawer — identity, never a control. Collapsed it shows its compact square, or nothing when the host gives none.
The Group and Columns panels beside the built-in Filter. `RecordsChrome.panels` is host-contributed and view-agnostic — the same cluster a table gets. OFF leaves Filter standing alone, which is a real posture: the tab row is absent at a single available control, because one control needs no tabs.
Shows when you open the view-options pane (tap Filter's control) — panels are TABS of that one pane, so the resting bar looks identical either way and only the pane's content differs — press Focus to make the preview interactive.
Dart for these values
screen_records_list() What you can do in it
Capability the preview already carries that no knob can express — you reach it by using the widget. Worth knowing before concluding something is missing: most of this is only clickable once the preview is at Focus size.
- **Search** — type in the field; the table narrows live. Collapsing search never clears the query, which survives as a removable
search:…chip. - **Filter** — the funnel opens the view-options pane. Facets are type-appropriate: enums and text offer distinct values, numbers a min/max, dates a From/To.
- **Chips** — every applied filter renders as a removable chip, and Clear all removes the lot.
- **Narrowed to zero** states the narrowing and offers Clear, rather than the author-a-record empty state.
What it is made of
The widgets this screen assembles, with their full API below. They used to live on a second entry of their own; one thing you choose gets one page.
AppList widget
The design-system List — a vertical collection of AppListItems, the compact-class posture for record collections (the swap counterpart of a `DataTable` below the table breakpoint). Supply rows via itemCount + itemBuilder (lazy, `ListView.builder` underneath) for large/dynamic collections, or…
Full dartdoc
The design-system List — a vertical collection of AppListItems, the compact-class posture for record collections (the swap counterpart of a `DataTable` below the table breakpoint). Supply rows via itemCount + itemBuilder (lazy, `ListView.builder` underneath) for large/dynamic collections, or children for a small fixed set of already-built AppListItems — exactly one of the two is required. Rows separate with a DesignTokens.hair hairline; the whole list scrolls. Selection mode: AppList does not itself own a selection set — the caller drives AppListItem.checkbox / AppListItem.selected per row (typically a leading AppCheckbox wired to toggle membership in the caller's own selection state, passed through itemBuilder or the children list). Kept deliberately simple, per the spec: tri-state select-all is a table-scope concern, not a list-scope one. Empty state: when the list has zero items (`itemCount == 0`, or children is empty), renders an AppEmptyState built from emptyLabel / emptyActionLabel / onEmptyAction instead of an empty scroll view. Adaptation strategy: **scale** (density only, via DesignTokens.rowV) — the same anatomy at every M3 window class; AppList is itself one half of the table/list **swap** a host performs at the `DataTable` breakpoint. ```dart AppList( itemCount: rows.length, itemBuilder: (context, i) => AppListItem( leading: AppIdChip(id: rowsi.id), title: rowsi.title, subtitle: rowsi.site, meta: rowsi.updatedLabel, selected: rowsi.id == activeId, danger: rowsi.status == 'Blocked', trailing: const KitIcon(KitGlyph.open), onTap: () => openRecord(rowsi.id), ), emptyLabel: 'No records match.', emptyActionLabel: 'Clear filters', onEmptyAction: clearFilters, ) ```
AppListdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| children * | List<Widget>? | — | The fixed row set for the plain AppList constructor. Null for the AppList.builder form. |
| emptyActionLabel | String? | null | Empty-state single action label. Renders only when onEmptyAction is also non-null. |
| emptyActionNavigates | bool | false | THE ACTION THAT ONLY OPENS A PAGE (`ActionStrip.d.ts`, #1749): the empty face's action pushes another screen and writes nothing, so it reports a link to assistive technology. Declared, never inferred from onEmptyAction. Default false. |
| emptyLabel | String | 'No records.' | Empty-state title — what the view would show. Rendered via AppEmptyState when the list has zero rows. |
| framed | bool | false | The grouped-settings frame (`List.d.ts` `framed`): a strong hairline on the top + inline edges and a soft hairline below, around the whole list — the settings-group posture for a framed cluster of rows on the surface wash. Off by default. |
| onEmptyAction | VoidCallback? | null | Empty-state single action callback. Renders only when emptyActionLabel is also non-null. |
| shrinkWrap | bool | false | When true, the list sizes itself to its rows and defers scrolling to an enclosing scroll view (NeverScrollableScrollPhysics) — the posture for a sectioned surface hosting several lists inside one outer scroll. Defaults to false: the list is its own scrollable, filling its viewport. |
AppList.builder
| Knob | Type | Default | What it does |
|---|---|---|---|
| emptyActionLabel | String? | null | Empty-state single action label. Renders only when onEmptyAction is also non-null. |
| emptyActionNavigates | bool | false | THE ACTION THAT ONLY OPENS A PAGE (`ActionStrip.d.ts`, #1749): the empty face's action pushes another screen and writes nothing, so it reports a link to assistive technology. Declared, never inferred from onEmptyAction. Default false. |
| emptyLabel | String | 'No records.' | Empty-state title — what the view would show. Rendered via AppEmptyState when the list has zero rows. |
| framed | bool | false | The grouped-settings frame (`List.d.ts` `framed`): a strong hairline on the top + inline edges and a soft hairline below, around the whole list — the settings-group posture for a framed cluster of rows on the surface wash. Off by default. |
| itemBuilder * | Widget Function(BuildContext context, int index)? | — | Builds row `index` for the AppList.builder form — typically an AppListItem, though any widget is accepted (a caller composing a section header inline, say). Null for the fixed-children form. |
| itemCount * | int? | — | Row count for the AppList.builder form. Null for the fixed-children form. |
| onEmptyAction | VoidCallback? | null | Empty-state single action callback. Renders only when emptyActionLabel is also non-null. |
| shrinkWrap | bool | false | When true, the list sizes itself to its rows and defers scrolling to an enclosing scroll view (NeverScrollableScrollPhysics) — the posture for a sectioned surface hosting several lists inside one outer scroll. Defaults to false: the list is its own scrollable, filling its viewport. |
AppListItem widget
One record row in the design-system List — the compact-class collection posture (phones, and any surface narrower than the `DataTable` swap threshold). Anatomy, leading to trailing: an optional selection-mode AppCheckbox (wired by AppList, never constructed directly by a caller composing a bare…
Full dartdoc
One record row in the design-system List — the compact-class collection posture (phones, and any surface narrower than the `DataTable` swap threshold). Anatomy, leading to trailing: an optional selection-mode AppCheckbox (wired by AppList, never constructed directly by a caller composing a bare AppListItem), an optional leading identity slot (an AppIdChip, a framed thumb, an avatar — any widget), a title (clamped to two lines, DesignTokens.bodyEm) with an optional subtitle clamped to subtitleMaxLines (default one line; DesignTokens.body/DesignTokens.ink60 — machine-adjacent supporting text, not DesignTokens.dataS; reserve the data font for meta), an optional right-aligned side column stacking meta (DesignTokens.dataS/DesignTokens.ink60 — timestamps, counts) over an optional status word (the spec's first-class status slot, rendered through the shared cell grammar's neutral enum cast; statusDanger takes danger color + em weight), and an optional trailing widget (a chevron, or a single always-visible action / `⋯` menu — **never hover-revealed**, per the constitution's row-actions rule). Selection grammar mirrors the table row exactly: selected paints a 2px DesignTokens.edge accent start-edge over a DesignTokens.rowSel tint wash; danger paints the same edge in DesignTokens.danger with no tint (blocked/error, per the spec's `rowDanger`). The two are mutually exclusive — a selected row's selected wins if both are somehow set, since an open/selected record is the more specific fact in the moment. Full state contract: enabled · hover (DesignTokens.rowHover wash, pointer enhancement only, suppressed while selected) · focus-visible (1px solid accent, zero offset) · pressed (1px translateY on the content, only when onTap is set) · disabled (no onTap → not tappable, no hover) · selected / danger (edge + tint, above). Row height is at least DesignTokens.touch (44 logical px). No ripple. ```dart AppListItem( leading: const AppIdChip(id: 'WO-2041'), title: 'Replace pump seal', subtitle: 'Site 4 — North wing', meta: 'upd 09:41', trailing: const KitIcon(KitGlyph.open), onTap: () => openRecord('WO-2041'), ) AppListItem( title: 'Blocked work order', danger: true, onTap: () => openRecord('WO-2099'), ) ```
AppListItemdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| action | Widget? | null | The row's ONE primary verb (`List.d.ts` `rowAction`/`action`, 2026-08-17) — an end-slot widget rendered BESIDE trailing, before it. For state-driven rows whose next action must be one tap (Download a ready file, Retry a failed run, Cancel a queued one) — burying the verb in the ⋯ menu was rejected… |
| checkbox | Widget? | null | Selection-mode checkbox, supplied and wired by AppList — a bare AppListItem composed directly by a caller may also pass one, but the common case is AppList wiring this from its own `selectable` state. Renders in a fixed leading slot before leading. |
| danger | bool | false | Blocked/error grammar: 2px DesignTokens.edge danger start-edge, no tint wash. Ignored when selected is also true. |
| leading | Widget? | null | Optional leading identity slot — an AppIdChip, a framed thumbnail, an avatar. Any widget; AppListItem does not constrain its shape beyond vertical centering. |
| meta | String? | null | Optional trailing machine text (DesignTokens.dataS/ DesignTokens.ink60) — a timestamp, a count, a status word. Right- aligned above trailing when both are present. |
| metaFormat | FormatRule<Object?>? | null | The resolved rule the meta line wears, every paint, the data font kept. Null: the plain line. |
| note | Widget? | null | A SECOND quiet line in the start column, under subtitle — a WIDGET, not a string, for the one shape a string cannot carry: a live FACT plus the inline affordance that acts on it. `List.d.ts` types the supporting slot as a NODE; this kit narrowed it to a `String`, which is right for the ordinary… |
| onTap | VoidCallback? | null | Row-tap callback — opens the record. Null renders a non-interactive row (no hover/focus/press paint, no Semantics tap action) — the shape AppList uses while in selection mode is still this same callback wired to toggle selection, so "non-interactive" is genuinely rare (a row with truly nothing to… |
| selected | bool | false | Selection/open-record grammar: 2px DesignTokens.edge accent start-edge over DesignTokens.rowSel tint. Same encoding a selected table row carries. |
| semanticLabel | String? | null | Overrides the row's accessible name. Defaults to title (the subtitle and meta are not concatenated in — a screen reader announces the record's name, not its full row text), then titleFormat's label when it has one ("Tank 4, Below minimum"). |
| status | String? | null | Optional status word — the spec's first-class `status` slot, stacked below meta in the right-aligned side column. A status is a WORD (encode once): rendered through the shared cell grammar (`appCellContent`, AppCellType.enumValue) as neutral DesignTokens.body/DesignTokens.ink60 — never a pill, never… |
| statusDanger | bool | false | Paints status in the danger role — DesignTokens.danger color AND em weight, the spec's `statusDanger` (color only where action is demanded). Ignored when status is absent. |
| subtitle | String? | null | Optional supporting text (DesignTokens.body/DesignTokens.ink60), e.g. a site name or parent record. Clamped to subtitleMaxLines (default one line), then ellipsized. |
| subtitleFormat | FormatRule<Object?>? | null | The resolved rule the supporting line (subtitle, `List.d.ts`'s `supporting`) wears, every paint ('tag' plates it on one line). Null: the plain line. |
| subtitleMaxLines | int | 1 | Line clamp for subtitle. Defaults to 1 (the canonical single-line supporting text); raise it when the supporting text must survive compact widths without truncating (e.g. a two-line description row). |
| title * | String | — | The record's primary label. Clamped to two lines (DesignTokens.bodyEm). |
| titleFormat | FormatRule<Object?>? | null | FORMAT RULES ON A ROW'S LINES (`List.d.ts`, #1006): the RESOLVED rule the title line wears — the resolver's output (resolveFormat), never a list. On the title 'tag' reads as 'ink' (a plated name is a badge) and `em` adds nothing. Null: the plain line, as before. |
| trailing | Widget? | null | Optional trailing widget — a chevron (`onTap`-only rows that merely navigate) or a single always-visible action / `⋯` menu. Never rendered conditionally on hover: the constitution bans hover-revealed row actions outright, and touch surfaces have no hover to reveal on. |
Types its knobs take
These are not things you choose — they are the shapes the knobs above expect. They live under this entry rather than in the navigation for exactly that reason.
FormatRule support
One FORMAT RULE (`FormatRules.d.ts` THE FORMAT RULE): a match and its effects. A rule list is ORDERED and the FIRST holding rule formats the value; a rule with no match always holds, so written last it is the column's default.
| Field | Type | Default | What it does |
|---|---|---|---|
| color | KitColor? | null | A palette name or a color parsed from data — on a map pin, its FILL. |
| em | bool | false | The word at the em weight. |
| glyph | KitIconRef? | null | A glyph — on a map pin, the head's glyph. |
| italic | bool | false | The word italic. |
| label | String? | null | What the rule means, in the host's words — read by a legend, a chip and the accessible name. |
| match | FormatMatch<T>? | null | The predicate, or null for "always holds" (the default rule). |
| muted | bool | false | The word at ink40 (ignored under FormatPaint.ink and FormatPaint.tag). |
| paint | FormatPaint | FormatPaint.mark | How color lands on a value (FormatPaint.mark by default). |
| strike | bool | false | The word struck through, in its own ink. |
Where it lives
| Class | File |
|---|---|
| AppList | packages/app_widgets/lib/src/kit/data/list.dart |
| AppListItem | packages/app_widgets/lib/src/kit/data/list.dart |