Skip to content
← Inventory

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) → AppShellBodyAppShellHeader + AppShellMain. Import from "@desk/ui/components/app-shell".

Material Library

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 AppShellMain per page — it owns the single <main> landmark; a dev warning fires on duplicates. Use AppShellBody (a <div>), never a second <main>.

  • Don't nest an AppShell inside another — it's the outermost frame.

  • Keep header/sidebar chrome OUTSIDE AppShellMain (in AppShellHeader / 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 renders AppShellSkipLink.

Props

AppShell

PropTypeDefaultDescription
height"fill" | "auto""auto"

Scroll architecture. "auto" (default): the document scrolls and headers are fixed; pair with AppShellMain offset="header". "fill": the shell fills the viewport (100dvh) and AppShellMain is an internal scroll container — dashboards, chat, anything with independent scroll regions.

sidebarbooleantrue

Mount the sidebar system (SidebarProvider + Cmd/Ctrl+B + CSS vars). Set false for header-only shells (a plain flex column).

defaultSidebarOpenboolean

Forwarded to SidebarProvider — initial open state (cookie-read app-side).

defaultSidebarWidthnumber

Forwarded to SidebarProvider — initial width in px (no-flash SSR).

skipLinkbooleantrue

Render the built-in skip link as the first child. Set false when the app root already renders one AppShellSkipLink for multiple route-group shells.

skipLinkLabelstring"Skip to content"

Skip-link text.

AppShellMain

PropTypeDefaultDescription
offset"header" | "none""header"

Pad under fixed headers in auto shells.

gutterbooleanfalse

Apply the shared horizontal page gutter (--page-gutter). Opt-in so apps that own their own gutter are unaffected; set false for edge-to-edge routes.

AppShellPanel

PropTypeDefaultDescription
side"left" | "right""right"

Which edge the panel docks to. Positioning comes from DOM order (place the panel before or after AppShellBody in the row); this only sets data-side for styling hooks.

variant"gutter" | "inline""inline"

"inline": an in-flow, sticky column that renders its children. "gutter": an empty, width-animated spacer that reserves room for a docked overlay (children are dropped and it is aria-hidden).

openbooleantrue

gutter animates its width between 0 and the panel width (lg: 300ms ease-out); inline is hidden when closed.

widthstring | number

Panel width — sets --app-shell-panel-width (a number is treated as px). Falls back to var(--sidepanel-footprint) (464px) for gutter and 25rem for inline.

AppShellSkipLink

PropTypeDefaultDescription
labelstring"Skip to content"

Link text.

targetIdstring"main-content"

Id of the target <main>.

Also exports

  • constAPP_SHELL_MAIN_ID
  • constappShellAnatomy
  • componentAppShellBanner
  • componentAppShellBody
  • componentAppShellHeader
  • typeAppShellPanelProps

Anatomy

AppShell

app-shell

Opinionated application frame: owns the skip link, the single <main> landmark, and the fill|auto scroll model

AppShellSkipLink

app-shell-skip-link

Bypass-blocks link to the main content (WCAG 2.4.1)

AppShellBanner

app-shell-banner

Optional announcement region above the header

AppShellHeader

app-shell-header

App header landmark, rendered outside <main>

AppShellBody

app-shell-body

Content column beside the sidebar

AppShellMain

app-shell-main

The single <main> landmark and scroll region

AppShellPanel

app-shell-panel

Region reserving a docked-overlay gutter (variant="gutter") or an in-flow sticky column (variant="inline")

Composition

Builds on

Sidebar