Theme
Customize the design system

Every control starts at the preset. Change one and every preview on the site follows, on this page and the next, until you reset it.

Contrast

Text against what it sits on. Below 4.5:1 fails WCAG AA for body text — the check a reviewer runs on any surface that takes a tone.

Waiting for the first preview to report its palette.

Corners

Sets all three radii at once. The individual values are under Geometry.

Brand 5

The accent seam an app actually brands itself through. Change these and you have made the decision a real `DesignTokens.graphite(...)` call makes.

Geometry 15

Square or rounded, and how heavy every line is. The radius row above the controls sets all three corners at once, because "what does square feel like" is one decision rather than three numbers.

Surfaces 17

Everything the app is painted ON — the ground behind the content (the frame paints it too), the cards over it, the well inset in a card, and the washes that show state.

Bands 10

The rail's head and the view bar beside it (one band across the top at 960 and wider), a pushed screen's bar, the compact bar and the compact nav bar — the band's fill, its rule, its hover and active faces, the resting field box on it, and the ink and accent control that sit on it. The rail's tree and its footer stay on the ground.

Text and controls 11

The ink ramp, the control outlines and the field's focus halo. Every pair the panel can break is watched by a contrast badge above.

Status 7

The three voices that mean something — danger, caution and ok — each as a foreground and a surface wash, danger with a border as well.

Data 5

The default pin head, its glyph and its halo, and the group bands. A pin's category color comes from the palette, named in the theme seed.

Map 11

The basemap's roles — land, water, parks, roads and their casings, buildings, labels — and the pending pin's gray. A raster basemap keeps its own imagery; these paint the map's plate and any kit-drawn map.

Scrollbars 6

Every scroller's bar: the thumb at rest and while hovered or dragged, the strip it rides, and the desk bar's thickness, side gap and corners. The desk half shows only while a fine pointer is attached; at touch the platform's own overlay keeps its size.

Advanced 6

Real controls that are not decisions — motion durations, the focus ring offset, the touch floor. Opens collapsed on purpose.

Take it with you

The Dart that reproduces what you are looking at. It is a real constructor call, because the configurator edits a real preset — what you copy is what an app can paste.

Screens

Records gallery

Reach for this when records are primarily images and the grid of tiles IS the collection. Rendered here with no image value, which is the real fallback state rather than a broken one. 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/Gallery.d.ts

12
Knobs

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.

screen_records_gallery
Knobs
Theme

Tile edge in logical pixels, clamped to 96–320.

Renders each tile's badge.

Shows when you tap a tile — selection paints on the ones you pick, and starts empty — press Focus to make the preview interactive.

Assembly

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.

View

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_gallery()

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.

The records chrome
  • **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.

AppGallery widget

The design-system Gallery — a responsive grid of image tiles, generic over the row type T. Any data, zero domain knowledge: every tile value comes from a typed accessor closure over T — imageValue (the stored image reference, fed straight to AppImage's resolution ladder), an optional caption…

Full dartdoc

The design-system Gallery — a responsive grid of image tiles, generic over the row type T. Any data, zero domain knowledge: every tile value comes from a typed accessor closure over T — imageValue (the stored image reference, fed straight to AppImage's resolution ladder), an optional caption (DesignTokens.dataS — identity text, not free prose), and an optional badge (a short count/status word rendered in the corner). There is no metadata blob and no string-keyed lookup anywhere in this file. Anatomy per tile: a hairline-framed (DesignTokens.hairS) thumb at radius DesignTokens.rS over an DesignTokens.ink4 void, a glyph fallback (KitGlyph.photo) when imageValue resolves to no image or fails to load, an optional caption line below the thumb, and an optional corner badge — mirrors the constitution's image-cell rule ("hairline-framed thumbs with glyph fallback, identity only") at gallery scale. Selection grammar (constitution, shared with `AppListItem` / `AppDataTable`'s row grammar): a selected tile paints a 2px DesignTokens.edge accent edge over a DesignTokens.rowSel tint wash — drawn as a frame around the whole tile (image + caption), since a gallery tile (unlike a row) has no start-edge to claim alone. State contract on every tile: enabled · hover (DesignTokens.ink4 wash, pointer enhancement only, suppressed while selected) · focus-visible (1px solid accent, zero offset) · pressed (1px translateY) · selected (accent edge + tint) · disabled (onTap null → not tappable, no hover/focus/press paint). Tap targets are at least DesignTokens.touch (44 logical px). No ripple. Selection: selectable + the controlled selected set + onSelectionChanged follow the same `initial*`-free controlled-set idiom as `AppDataTable`. A tile's onTap fires unconditionally (identity is verbatim T equality — T must have value/identity equality suitable for a `Set`); when selectable is true the tap also toggles the tile's membership by calling onSelectionChanged with the next set. Empty: when items is empty, renders an AppEmptyState built from emptyLabel + emptyActionLabel / onEmptyAction instead of a bare grid. Archetypes: the default constructor is the **browse grid** — a vertical `GridView` that fills the space it is given. AppGallery.rail is the **filmstrip** — a horizontally-scrolling, virtualized `ListView` of the same tiles at a fixed tileExtent, sized to exactly one tile tall so it drops into a bounded slot (a detail section, a header strip) without the host computing a height. One tile primitive, two thin layout policies: a tile reads, behaves, and sounds identical in either. Sizing: tileExtent is the target square edge for SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent — the grid packs as many columns as fit at or under that extent, so it is inherently responsive across the M3 window classes without a breakpoint switch (adaptation strategy: **scale**, density only — the same anatomy at every class). On the rail the same value is the tile's *exact* width, and the rail's height falls out of it, so the two archetypes share one geometry knob. ```dart AppGallery<Photo>( items: photos, imageValue: (p) => p.storedRef, caption: (p) => p.label, badge: (p) => p.pinCount > 0 ? '${p.pinCount}' : null, selectable: true, selected: selection, onSelectionChanged: (s) => setState(() => selection = s), onTap: (p) => openViewer(p), emptyLabel: 'No photos on this record.', ) ```

AppGallerydefault constructor

Knob Type Default What it does
badge String? Function(T item)? null Typed accessor for a short corner badge word (a count or status word — "encode once," never a re-encoding of the caption). Returning null for a given row renders no badge on that tile; the accessor itself may also be null to disable badges entirely.
caption String Function(T item)? null Typed accessor for the tile's identity caption (DesignTokens.dataS — machine/identity text, e.g. a filename or page label, not free prose). Null renders no caption line.
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 images.' Empty-state title — what the view would show. Rendered via AppEmptyState when items is empty.
imageValue * String Function(T item) — Typed accessor — the stored image reference for a row, fed verbatim to AppImage's resolution ladder (`staged://` / `asset:` / `http(s)://` / origin-relative / blank). A closure over the typed row — never a map lookup.
items * List<T> — The rows to render. Empty renders the AppEmptyState.
onEmptyAction VoidCallback? null Empty-state single action callback. Renders only when emptyActionLabel is also non-null.
onSelectionChanged ValueChanged<Set<T>>? null Fires with the next selection set when a tile is tapped while selectable is true. Null renders read-only selection highlighting (a tile in selected still shows the accent edge, but tapping does not mutate it).
onTap void Function(T item)? null Fires with the tapped row. When selectable is true this also drives the selection toggle (see onSelectionChanged); when false it is the sole tap channel (e.g. opening a viewer). Null renders non-interactive tiles (no hover/focus/press paint, no Semantics tap action).
selectable bool false When true, tapping a tile also toggles its membership in selected via onSelectionChanged, and a selected tile paints the accent edge + tint. When false, selected and onSelectionChanged are ignored.
selected Set<T> const {} The controlled selection set. Ignored when selectable is false.
tileExtent double _defaultTileExtent The target tile edge in logical pixels (SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent) — the grid packs as many columns as fit at or under this extent. On AppGallery.rail it is the tile's exact width, and the rail's height is derived from it. Defaults to _defaultTileExtent (112).

AppGallery.rail

Knob Type Default What it does
badge String? Function(T item)? null Typed accessor for a short corner badge word (a count or status word — "encode once," never a re-encoding of the caption). Returning null for a given row renders no badge on that tile; the accessor itself may also be null to disable badges entirely.
caption String Function(T item)? null Typed accessor for the tile's identity caption (DesignTokens.dataS — machine/identity text, e.g. a filename or page label, not free prose). Null renders no caption line.
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 images.' Empty-state title — what the view would show. Rendered via AppEmptyState when items is empty.
imageValue * String Function(T item) — Typed accessor — the stored image reference for a row, fed verbatim to AppImage's resolution ladder (`staged://` / `asset:` / `http(s)://` / origin-relative / blank). A closure over the typed row — never a map lookup.
items * List<T> — The rows to render. Empty renders the AppEmptyState.
onEmptyAction VoidCallback? null Empty-state single action callback. Renders only when emptyActionLabel is also non-null.
onSelectionChanged ValueChanged<Set<T>>? null Fires with the next selection set when a tile is tapped while selectable is true. Null renders read-only selection highlighting (a tile in selected still shows the accent edge, but tapping does not mutate it).
onTap void Function(T item)? null Fires with the tapped row. When selectable is true this also drives the selection toggle (see onSelectionChanged); when false it is the sole tap channel (e.g. opening a viewer). Null renders non-interactive tiles (no hover/focus/press paint, no Semantics tap action).
selectable bool false When true, tapping a tile also toggles its membership in selected via onSelectionChanged, and a selected tile paints the accent edge + tint. When false, selected and onSelectionChanged are ignored.
selected Set<T> const {} The controlled selection set. Ignored when selectable is false.
tileExtent double _defaultTileExtent The target tile edge in logical pixels (SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent) — the grid packs as many columns as fit at or under this extent. On AppGallery.rail it is the tile's exact width, and the rail's height is derived from it. Defaults to _defaultTileExtent (112).

Where it lives

Class File
AppGallery packages/app_widgets/lib/src/kit/data/gallery.dart