Skip to content

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.

PropTypeDefaultDescription
brandResourceStr''
brandIconstring'sparkles'
brandDetailResourceStr | undefinedundefined
navSparkShellNavGroup[][]
controllerSparkSidebarState | undefinedundefinedyour own sidebar state object (headless control); one is created when omitted
value!!string | undefinedundefinedactive nav item id
defaultValuestring''
titleResourceStr | undefinedundefinedtop bar title (default: the active nav item's title)
subtitleResourceStr | undefinedundefined
actionsSparkTopbarAction[][]
searchbooleanfalse
searchPlaceholderResourceStr$r('app.string.spark_search_placeholder')
userSparkShellUser | undefinedundefined
footerTextResourceStr | undefinedundefined
variantSidebarVariant
SidebarVariant = 'sidebar' | 'floating' | 'inset'
'sidebar'
collapsibleSidebarCollapsible
SidebarCollapsible = 'offcanvas' | 'icon' | 'none'
'icon'
sideSidebarSide
SidebarSide = 'left' | 'right'
'left'
sidebarWidthnumber0docked sidebar width (vp); 0 = the --sidebar-width token, else 256
minInsetWidthnumber360input of layoutPolicy: the content area should keep at least this width (vp)
closeOnSelectbooleantrue
motionKeystring'sidebar'
open!!boolean | undefinedundefined
defaultOpenbooleantrueinitial docked state; feed a persisted value here and save it in onOpenChange
showTopbarbooleantrueshow the built-in top bar (false: bring your own via headerBuilder or the content)
topbarLeadingTopbarLeading
TopbarLeading = 'auto' | 'trigger' | 'back' | 'none'
'auto'top bar leading control: 'auto' (sidebar trigger), 'none' when e.g. a title bar already hosts it
topbarBlurbooleantruetranslucent top bar with content scrolling underneath
scrollablebooleantruewrap the content in the padded, centred scroller (false: the content fills the area as is)
safeAreabooleantruepad the window safe area (immersive pages)
maxContentWidthnumber | undefinedundefinedmax width of the page content (0 = unlimited; unset = --app-shell-content-max-width, else 1200)
tokensAppShellTokens{}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-*.
titleBarBuilderWrappedBuilder<[]> | undefinedundefinedregion above the whole shell, e.g. an STitleBar for free-form windows
sidebarHeaderBuilderWrappedBuilder<[]> | undefinedundefinedunder the brand in the sidebar header (team switcher, search…)
sidebarContentBuilderWrappedBuilder<[]> | undefinedundefinedafter the nav groups in the scrolling sidebar content
sidebarFooterBuilderWrappedBuilder<[]> | undefinedundefinedunder the user in the sidebar footer
navItemBuilderWrappedBuilder<[SparkShellNavItem]>SPARK_SHELL_NAV_ITEMrenders one nav item (default: wrapBuilder(sparkShellNavItemBuilder))
navGroupLabelBuilderWrappedBuilder<[SparkShellNavGroup]>SPARK_SHELL_NAV_GROUP_LABELrenders a group's label (default: wrapBuilder(sparkShellNavGroupLabelBuilder))
topbarLeadingBuilderWrappedBuilder<[]> | undefinedundefinedextra top bar leading content (STopbar leadingBuilder)
topbarTitleBuilderWrappedBuilder<[]> | undefinedundefinedextra top bar title content (STopbar titleBuilder)
topbarActionsBuilderWrappedBuilder<[]> | undefinedundefinedtrailing top bar content after actions (plain icon buttons fit actions better)
headerBuilderWrappedBuilder<[]> | undefinedundefinedunder the top bar, above the content (tabs, filters…; also a custom bar with showTopbar: false)
footerBuilderWrappedBuilder<[]> | undefinedundefinedafter the content (after footerText)
EventTypeDefaultDescription
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() => { }
layoutPolicySparkSidebarLayoutPolicysparkSidebarDefaultPolicy
onPresentationChange(presentation: SidebarPresentation) => void
SidebarPresentation = 'docked' | 'rail' | 'sheet'
() => { }
onOpenMobileChange(open: boolean) => void() => { }
onOpenChange(open: boolean) => void() => { }

Slots: children (trailing closure)

MIT Licensed