Skip to content

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).

PropTypeDefaultDescription
controllerSparkListDetailState | undefinedundefinedyour own state object (headless control); one is created when omitted
showDetail!!boolean | undefinedundefined
defaultShowDetailbooleanfalse
listWidthnumber0list pane width when side by side (vp); 0 = the --list-detail-list-width token, else 320
tokensListDetailTokens{}per-instance look (ADR D13): instance → --list-detail-* tokens → recipe
minDetailWidthnumber360the detail needs at least this width to sit next to the list (vp)
motionKeystring'list-detail'
EventTypeDefaultDescription
onShowDetailChange(show: boolean) => void() => { }
layoutPolicySparkSplitPolicysparkListDetailDefaultPolicyside 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.

PropTypeDefaultDescription
titleResourceStr | undefinedundefined
showBackbooleantrueshow the back / title bar when narrow (default)
backLabelResourceStr$r('app.string.spark_back')
emptybooleanfalseNothing 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.
emptyIconstring'inbox'lucide icon of the empty state
emptyTitleResourceStr$r('app.string.spark_nothing_selected')
emptyDescriptionResourceStr | undefinedundefined
actionsSparkTopbarAction[][]icon buttons at the trailing end of the title bar (onAction(id))
headerBuilderWrappedBuilder<[]> | undefinedundefinedCustom trailing title-bar content after actions, as wrapBuilder(myHeader) of a global @Builder (no this: render a component or read shared state).
EventTypeDefaultDescription
onBack() => boolean() => falseback 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)

MIT Licensed