App Shell

The app's top-level page shell: a header band across the top, then a nav column beside the page content. The page itself is what scrolls — the header is sticky, so it holds while content travels under its translucent, blurred backing, and the light-seam along its bottom edge is the app's light source (see .surface-sheen). lightSource aims it at the pointer through --light-x, so the hot spot slides along the seam and every surface's wash swings with it. The header carries the brand lockup — Brand unless logo replaces it — plus (at md and up) search, DisplayControls and the account menu; PageHeader still owns page-level actions underneath it. With nav, a Sidebar of labelled entries stands to the left of main at md and up — always expanded, never collapsed to icons, since a label read at a glance beats a tooltip. It sits straight on the page background with no surface of its own, because navigation is not content and only main's cards should read as panels. It sticks below the header and keeps the leftover viewport height, so a long list scrolls inside the column rather than dragging the page. Header, nav and main are all capped at --shell-max-w (1650px) and centered together by default, so on wide screens the shell hugs the content instead of the viewport edge. Set contained={false} to drop the cap for pages whose content wants the full viewport instead. Below md, search and the account menu move instead into the same Sidebar inside a drawer opened from a hamburger in the header — search on top, the nav list filling the middle, UserMenu and DisplayControls pinned to the bottom. Use once per app, wrapping the routed page content in children.

What it looks like

One per app, wrapping the routed page. The header is sticky and translucent, so content travels under it, and the light-seam along its bottom edge is the app's light source — lightSource aims it at the pointer, and every surface's wash swings with it. This site is inside one right now: the header, the sidebar and the search control above are all AppShell. Everything below is a still frame of the same thing, so the parts can be pointed at.

firmly. / Admin
controls · account

Dashboard

Today across every channel.

Revenue

A product with a mark of its own

wordmark covers a product that renames the lockup; the logo snippet replaces it outright, for one that draws its own. This site is the case: the header above renders ground's mark instead of Brand. Leave it unset and every app on the platform keeps firmly., which is the point — an app never decides what the platform's brand looks like.

How an app mounts it

nav is the only thing that decides whether there is a sidebar at all — omit it, or pass mode="embedded", and the shell renders the header and content alone, which is what an app hosted inside another shell wants. The search snippet is rendered in the header at md and up, and at the top of the mobile drawer below that. theme comes from createTheme() and is forwarded to DisplayControls; the text scale is owned by the shell itself, since nothing outside it reads the level back.

A real mount is a handful of lines in the root layout — see this app's own src/routes/+layout.svelte. Everything else on a page comes from PageHeader downwards.

API

Prop Type Default Description
user {email: string, name?: string}

Signed-in user, forwarded to UserMenu.

theme {resolved: 'light'|'dark', toggle: () => void}

Theme controller, forwarded to DisplayControls.

appName string

Child app name rendered in the Brand.

wordmark string

Forwarded to Brand, and on to Logo. A product with a wordmark of its own sets this and omits appName.

env string

Environment name shown as a chip, hidden in production.

beta boolean false

Marks the app as pre-release with a Beta chip.

homeHref string '/'

Route treated as the app root, forwarded to Sidebar.

logoHref string '/'

Href for the logo.

nav import('../sidebar/sidebar.svelte').NavGroup[] []

Navigation groups. Omit to render the shell without navigation.

sections Array<{href: string, label: string, current?: boolean}> []

The app's top-level areas, rendered as a row in the header and at the top of the mobile drawer. Reach for it when one Sidebar would otherwise list everything the app has; nav then carries only the current area's entries. current marks which area the route is in — pass it when an area spans more than one path prefix, otherwise the shared route rule decides from href.

onLogout () => void

Forwarded to UserMenu's logout action.

mode 'standalone'|'embedded' 'standalone'

standalone renders navigation; embedded omits it (e.g. when hosted inside another app's shell).

contained boolean true

Caps the header and content at --shell-max-w (1650px), centered. Set false for pages whose content wants the full viewport instead — a dashboard dense with charts, say.

logo import('svelte').Snippet

Replaces the default Brand lockup in the header. For a product with a mark of its own to draw; the apps built on the platform leave it unset and get firmly..

search import('svelte').Snippet

Quick-access control rendered in the header at md and up, and at the top of the mobile Sidebar drawer below that.

navTop import('svelte').Snippet

Rendered above the nav groups in Sidebar, standing and in the drawer alike — a control for the current section as a whole rather than one entry in it.

children import('svelte').Snippet

Routed page content rendered in the scrollable main area.