App Shell
Sidebar + top bar + scrolling content in one block — the layout of this gallery, driven by data. It pads immersive safe areas itself, scrolls content under a translucent top bar and scrolls back to the top when the page changes.
Usage
Approximate web preview — the HarmonyOS rendering is the reference.
API
Extracted from the @Param / @Event / @BuilderParam declarations in the library sources.
SAppShell
Ready-made application shell, and a thin data-driven layer over SSidebarProvider / SSidebar / SSidebarInset / STopbar: a sidebar (brand, grouped nav, user footer), a top bar (sidebar trigger, title of the active page, search, actions) and the page content, with an optional footer.
Breakpoint-free: layoutPolicy decides from the shell's own width whether the sidebar is docked, an icon rail or a sheet (default: docked when it fits, else a sheet; sparkSidebarRailPolicy() adds a rail step). Immersive-ready (safeArea, default on): the sidebar, top bar and content pad the window insets themselves (ADR D10 — expandSafeArea cannot cross custom components).
Extension points (docs/design/blocks-extensibility.md): controller (own the SparkSidebarState), layoutPolicy, every state change as an event, a WrappedBuilder region for each area (titleBarBuilder, sidebarHeaderBuilder, sidebarContentBuilder, sidebarFooterBuilder, navItemBuilder(item), navGroupLabelBuilder(group), topbarLeadingBuilder, topbarTitleBuilder, topbarActionsBuilder, headerBuilder, footerBuilder), showTopbar / scrollable to opt out of the built-in bar and scroller. Regions are additive to the data props (leave brand / user / nav empty to replace them). The page is the only @BuilderParam (trailing closure); regions take wrapBuilder(globalBuilder) — a global builder has no this, so render one of your components there or read shared state (own the controller to reach the sidebar). For anything further, compose SSidebarProvider / SSidebar / SSidebarInset / STopbar directly.
| Prop | Type | Default | Description |
|---|---|---|---|
brand | ResourceStr | '' | |
brandIcon | string | 'sparkles' | |
brandDetail | ResourceStr | undefined | undefined | |
nav | SparkShellNavGroup[] | [] | |
controller | SparkSidebarState | undefined | undefined | your own sidebar state object (headless control); one is created when omitted |
value!! | string | undefined | undefined | active nav item id |
defaultValue | string | '' | |
title | ResourceStr | undefined | undefined | top bar title (default: the active nav item's title) |
subtitle | ResourceStr | undefined | undefined | |
actions | SparkTopbarAction[] | [] | |
search | boolean | false | |
searchPlaceholder | ResourceStr | $r('app.string.spark_search_placeholder') | |
user | SparkShellUser | undefined | undefined | |
footerText | ResourceStr | undefined | undefined | |
variant | SidebarVariantSidebarVariant = 'sidebar' | 'floating' | 'inset' | 'sidebar' | |
collapsible | SidebarCollapsibleSidebarCollapsible = 'offcanvas' | 'icon' | 'none' | 'icon' | |
side | SidebarSideSidebarSide = 'left' | 'right' | 'left' | |
sidebarWidth | number | 0 | docked sidebar width (vp); 0 = the --sidebar-width token, else 256 |
minInsetWidth | number | 360 | input of layoutPolicy: the content area should keep at least this width (vp) |
closeOnSelect | boolean | true | |
motionKey | string | 'sidebar' | |
open!! | boolean | undefined | undefined | |
defaultOpen | boolean | true | initial docked state; feed a persisted value here and save it in onOpenChange |
showTopbar | boolean | true | show the built-in top bar (false: bring your own via headerBuilder or the content) |
topbarLeading | TopbarLeadingTopbarLeading = 'auto' | 'trigger' | 'back' | 'none' | 'auto' | top bar leading control: 'auto' (sidebar trigger), 'none' when e.g. a title bar already hosts it |
topbarBlur | boolean | true | translucent top bar with content scrolling underneath |
scrollable | boolean | true | wrap the content in the padded, centred scroller (false: the content fills the area as is) |
safeArea | boolean | true | pad the window safe area (immersive pages) |
maxContentWidth | number | undefined | undefined | max width of the page content (0 = unlimited; unset = --app-shell-content-max-width, else 1200) |
tokens | AppShellTokens | {} | Per-instance tokens (ADR D13): content max width / padding plus sidebar ({@link SidebarTokens}) and topbar ({@link TopbarTokens}) for the parts the shell builds. Theme: --app-shell-*, --sidebar-*, --topbar-*. |
titleBarBuilder | WrappedBuilder<[]> | undefined | undefined | region above the whole shell, e.g. an STitleBar for free-form windows |
sidebarHeaderBuilder | WrappedBuilder<[]> | undefined | undefined | under the brand in the sidebar header (team switcher, search…) |
sidebarContentBuilder | WrappedBuilder<[]> | undefined | undefined | after the nav groups in the scrolling sidebar content |
sidebarFooterBuilder | WrappedBuilder<[]> | undefined | undefined | under the user in the sidebar footer |
navItemBuilder | WrappedBuilder<[SparkShellNavItem]> | SPARK_SHELL_NAV_ITEM | renders one nav item (default: wrapBuilder(sparkShellNavItemBuilder)) |
navGroupLabelBuilder | WrappedBuilder<[SparkShellNavGroup]> | SPARK_SHELL_NAV_GROUP_LABEL | renders a group's label (default: wrapBuilder(sparkShellNavGroupLabelBuilder)) |
topbarLeadingBuilder | WrappedBuilder<[]> | undefined | undefined | extra top bar leading content (STopbar leadingBuilder) |
topbarTitleBuilder | WrappedBuilder<[]> | undefined | undefined | extra top bar title content (STopbar titleBuilder) |
topbarActionsBuilder | WrappedBuilder<[]> | undefined | undefined | trailing top bar content after actions (plain icon buttons fit actions better) |
headerBuilder | WrappedBuilder<[]> | undefined | undefined | under the top bar, above the content (tabs, filters…; also a custom bar with showTopbar: false) |
footerBuilder | WrappedBuilder<[]> | undefined | undefined | after the content (after footerText) |
| Event | Type | Default | Description |
|---|---|---|---|
onValueChange | (value: string) => void | () => { } | |
onNavigate | (value: string) => void | () => { } | every activation of a nav item, even the current one (router hook) |
onAction | (id: string) => void | () => { } | |
onSearch | (value: string) => void | () => { } | |
onSearchChange | (value: string) => void | () => { } | |
layoutPolicy | SparkSidebarLayoutPolicy | sparkSidebarDefaultPolicy | |
onPresentationChange | (presentation: SidebarPresentation) => voidSidebarPresentation = 'docked' | 'rail' | 'sheet' | () => { } | |
onOpenMobileChange | (open: boolean) => void | () => { } | |
onOpenChange | (open: boolean) => void | () => { } |
Slots: children (trailing closure)