List Detail
Master / detail without routing. Side by side when the container fits both panes; one pane at a time when it does not — selecting pushes the detail over the list, and its back button pops it.
Usage
Approximate web preview — the HarmonyOS rendering is the reference.
API
Extracted from the @Param / @Event / @BuilderParam declarations in the library sources.
SListDetail
Canonical list-detail layout (Material "list-detail", iPad split view) — no routing involved. Side by side when the container fits listWidth + minDetailWidth; otherwise a single pane where the detail is pushed over the list (slide, motion key 'list-detail') and SListDetailPane shows a back button. Both panes stay mounted, so the list keeps its scroll position.
Narrow + detail shown: wire the page's onBackPress to showDetail = false (or controller.back()) for the system back gesture.
Extension points: controller (own the state), layoutPolicy (replace the side-by-side decision), onWideChange, motionKey; SListDetailPane onBack (intercept), title-bar actions + onAction and a headerBuilder region (wrapBuilder(globalBuilder)), empty + emptyIcon / emptyTitle / emptyDescription (or branch inside the trailing closure for a custom empty state).
| Prop | Type | Default | Description |
|---|---|---|---|
controller | SparkListDetailState | undefined | undefined | your own state object (headless control); one is created when omitted |
showDetail!! | boolean | undefined | undefined | |
defaultShowDetail | boolean | false | |
listWidth | number | 0 | list pane width when side by side (vp); 0 = the --list-detail-list-width token, else 320 |
tokens | ListDetailTokens | {} | per-instance look (ADR D13): instance → --list-detail-* tokens → recipe |
minDetailWidth | number | 360 | the detail needs at least this width to sit next to the list (vp) |
motionKey | string | 'list-detail' |
| Event | Type | Default | Description |
|---|---|---|---|
onShowDetailChange | (show: boolean) => void | () => { } | |
layoutPolicy | SparkSplitPolicy | sparkListDetailDefaultPolicy | side by side (true) or one pane (false), from the measured width (replaceable) |
onWideChange | (wide: boolean) => void | () => { } | the layout switched between side by side and one pane |
Slots: children (trailing closure)
SListPane
The list (master) pane.
Slots: children (trailing closure)
SListDetailPane
The detail pane. Side by side it fills the rest of the width (with an optional title bar); narrow it slides over the list and its title bar gets a back button that pops back to the list.
| Prop | Type | Default | Description |
|---|---|---|---|
title | ResourceStr | undefined | undefined | |
showBack | boolean | true | show the back / title bar when narrow (default) |
backLabel | ResourceStr | $r('app.string.spark_back') | |
empty | boolean | false | Nothing selected: show the empty state (emptyIcon / emptyTitle / emptyDescription, an SEmpty) instead of the content. For a custom empty state leave this false and branch inside the trailing closure. |
emptyIcon | string | 'inbox' | lucide icon of the empty state |
emptyTitle | ResourceStr | $r('app.string.spark_nothing_selected') | |
emptyDescription | ResourceStr | undefined | undefined | |
actions | SparkTopbarAction[] | [] | icon buttons at the trailing end of the title bar (onAction(id)) |
headerBuilder | WrappedBuilder<[]> | undefined | undefined | Custom trailing title-bar content after actions, as wrapBuilder(myHeader) of a global @Builder (no this: render a component or read shared state). |
| Event | Type | Default | Description |
|---|---|---|---|
onBack | () => boolean | () => false | back button pressed; return true when handled (e.g. pop your own stack) to skip the default pop |
onAction | (id: string) => void | () => { } |
Slots: children (trailing closure)