AppShell
Opinionated application frame. Owns the skip link, the single <main>
landmark (via AppShellMain), and the height="auto"|"fill" scroll model —
published as data-shell-height + a group/app-shell class so every part
styles itself with group-data-[shell-height=…]/app-shell: variants (no
context; parts stay server-compatible).
Compose with children (house style): AppShell → sidebar (optional) →
AppShellBody → AppShellHeader + AppShellMain.
Import from "@desk/ui/components/app-shell".
Materials
128 in library
Calacatta Gold marble
Reclaimed oak plank
Brushed stainless steel
Terracotta hex tile
Sage linen weave
Board-formed concrete
Usage
@desk/ui/components/app-shell
<AppShell height="fill" sidebar={false} skipLink={false}>
<AppShellBody>
<AppShellHeader>
<Box className="flex h-12 items-center border-border border-b bg-background px-4 font-medium text-sm">
Material Library
</Box>
</AppShellHeader>
<Box className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
<PageHeader>
<PageHeaderTitle size="lg" subtitle="128 in library">
Materials
</PageHeaderTitle>
<PageHeaderActions>
<Button icon={Plus} size="sm" tone="brand">
Add
</Button>
</PageHeaderActions>
</PageHeader>
<Stack className="pt-3" gap="xs">
{RECENT.map((material) => (
<Text className="text-muted-foreground" key={material} size="sm">
{material}
</Text>
))}
</Stack>
</Box>
</AppShellBody>
</AppShell>Best practices
Choose
height="fill"for dashboards/chat with independent scroll regions;"auto"for content pages that grow with the document.Render exactly one
AppShellMainper page — it owns the single<main>landmark; a dev warning fires on duplicates. UseAppShellBody(a<div>), never a second<main>.Don't nest an
AppShellinside another — it's the outermost frame.Keep header/sidebar chrome OUTSIDE
AppShellMain(inAppShellHeader/ the sidebar slot) so the banner/nav landmarks sit beside, not inside,<main>.One site-wide skip link only: set
skipLink={false}when a parent layout already rendersAppShellSkipLink.
Props
AppShell
| Prop | Type | Default | Description |
|---|---|---|---|
| height | "fill" | "auto" | "auto" | Scroll architecture. |
| sidebar | boolean | true | Mount the sidebar system ( |
| defaultSidebarOpen | boolean | Forwarded to | |
| defaultSidebarWidth | number | Forwarded to | |
| skipLink | boolean | true | Render the built-in skip link as the first child. Set |
| skipLinkLabel | string | "Skip to content" | Skip-link text. |
AppShellMain
| Prop | Type | Default | Description |
|---|---|---|---|
| offset | "header" | "none" | "header" | Pad under fixed headers in |
| gutter | boolean | false | Apply the shared horizontal page gutter ( |
AppShellPanel
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "left" | "right" | "right" | Which edge the panel docks to. Positioning comes from DOM order (place the
panel before or after |
| variant | "gutter" | "inline" | "inline" |
|
| open | boolean | true |
|
| width | string | number | Panel width — sets |
AppShellSkipLink
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Skip to content" | Link text. |
| targetId | string | "main-content" | Id of the target |
Also exports
- const
APP_SHELL_MAIN_ID - const
appShellAnatomy - component
AppShellBanner - component
AppShellBody - component
AppShellHeader - type
AppShellPanelProps
Anatomy
AppShell
app-shellOpinionated application frame: owns the skip link, the single <main> landmark, and the fill|auto scroll model
AppShellSkipLink
app-shell-skip-linkBypass-blocks link to the main content (WCAG 2.4.1)
AppShellBanner
app-shell-bannerOptional announcement region above the header
AppShellHeader
app-shell-headerApp header landmark, rendered outside <main>
AppShellBody
app-shell-bodyContent column beside the sidebar
AppShellMain
app-shell-mainThe single <main> landmark and scroll region
AppShellPanel
app-shell-panelRegion reserving a docked-overlay gutter (variant="gutter") or an in-flow sticky column (variant="inline")
Composition
Builds on