App Shell
The page-level application frame — a tinted canvas with a sidebar, content panel, and optional detail aside.
@garn/ui/app-shellDefault
The framed/inset look: tinted canvas + one floating content panel.
Active opportunities
78
Deals closed
15
Avg response
4.2d
Engagement
62%
| Company | Domain | Status | |
|---|---|---|---|
| Apple | apple.com | To contact | |
| Microsoft | microsoft.com | Follow-up | |
| Amazon | amazon.com | In negotiation | |
| google.com | Complete | ||
| Tesla | tesla.com | To contact |
Resizable
Multi-pane composed with resizable drag handles (a pattern, not shell API).
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add app-shell- Registry
app-shell- Source
- app-shell.tsx
- Deps
class-variance-authoritylucide-react- Registry deps
utilsuse-controllable-stateuse-merged-refuse-resize-observeruse-event-listenerbadgebuttoncommanddialogsheet
Import
import { AppShell } from "@garn/ui/app-shell";Props
Generated from the component's TypeScript types.
appearance- Description
- No description.
- Type
"flush" | "framed"- Default
flush
collapsible- Description
- No description.
- Type
"icon" | "none" | "offcanvas"- Default
none
placement- Description
- No description.
- Type
"docked" | "floating"- Default
docked
adaptive- Description
- Adapt the layout to the shell's own width — auto-rail the sidebar and float the aside/sidebar as Sheets on narrow. `false` pins the shell `expanded` (the pre-adaptive static behavior). Default `true`.
- Type
boolean
asideOpen- Description
- Controlled detail-aside open state (e.g. for a Peek that opens on row click).
- Type
boolean
defaultAsideOpen- Description
- Initial detail-aside open state (uncontrolled). Default open.
- Type
boolean
defaultOpen- Description
- Initial sidebar intent (uncontrolled). `undefined` = **auto**: the size class decides (expanded desktop, rail at medium). Seed from the `app-shell:sidebar` cookie server-side for an SSR no-flash restore.
- Type
boolean
defaultSize- Description
- Initial size class before measurement (uncontrolled). Seed from the `app-shell:size` cookie server-side to avoid a first-paint flash.
- Type
"compact" | "expanded" | "medium"
mainId- Description
- Skip-link target; must match the `id` on AppShellMain (shared default).
- Type
string
onAsideOpenChange- Description
- No description.
- Type
(open: boolean) => void
onOpenChange- Description
- No description.
- Type
(open: boolean) => void
onSizeChange- Description
- No description.
- Type
(size: "compact" | "medium" | "expanded") => void
open- Description
- Controlled desktop sidebar open state. Always honored (explicit intent).
- Type
boolean
size- Description
- Controlled size class — mostly observed; set it to force a tier (tests, docs).
- Type
"compact" | "expanded" | "medium"
Plus 280 inherited native <div> attributes.
AppShellSidebar
collapsible- Description
- Desktop collapse behavior. `icon` shrinks to a rail; `offcanvas` hides it.
- Type
"icon" | "none" | "offcanvas"
Plus 281 inherited native HTML attributes.
AppShellNav
label- Description
- Optional section heading rendered above the items; hidden on the rail.
- Type
React.ReactNode
Plus 280 inherited native HTML attributes.
AppShellNavItem
active- Description
- Marks the current destination: soft fill + `aria-current="page"`.
- Type
boolean
count- Description
- Trailing count — wrapped in a soft neutral Badge; hidden on the rail.
- Type
React.ReactNode
icon- Description
- Leading icon; stays centered when the sidebar collapses to a rail.
- Type
React.ReactNode
Plus 301 inherited native HTML attributes.
AppShellSelectionBar
clearLabel- Description
- No description.
- Type
string
countRequired- Description
- No description.
- Type
number
onClear- Description
- No description.
- Type
() => void
Plus 281 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="app-shell"]| Property | Token | Tier |
|---|---|---|
gutter | --garn-space-8 | primitive · air |
regionPadding | --garn-pad-panel | semantic · air |
headerGap | --garn-gap-inline | semantic · air |
radius | --garn-radiusbrandable | semantic |
canvas | --garn-background | semantic |
raised | --garn-card | semantic |
hairline | --garn-border | semantic |
Overview
AppShell is the top-level frame of an application screen — a dashboard, console, inbox, or records view that needs a persistent sidebar, header, and content area. AppShell is the root + provider (owns the appearance, collapse state, and the adaptive size class); AppShellSidebar holds primary nav (built from AppShellNav groups + AppShellNavItems — collapsible rail/offcanvas inline, a drawer at the compact tier), and AppShellContent groups AppShellHeader / AppShellToolbar / main / footer. By default it's adaptive: it measures its own width and auto-rails the sidebar at medium and floats the sidebar + aside as Sheets at compact — no consumer media queries. It's the scaffold for a whole app view — for grouping content within a page, use Card.
Guidelines
Use AppShell for the app frame, not for page sections. It's the one persistent chrome around an authenticated product surface; don't nest shells or reach for it to box a widget (that's a Card). Keep one sidebar and one header per shell.
Make navigation survive collapse and mobile. When the sidebar collapses to a rail or moves into a drawer, keep nav items reachable and labelled (icons need accessible names); preserve the user's collapse preference. Provide the skip-link the shell ships so keyboard users can jump to main.
Keep the header and toolbar earning their rows. Put wayfinding (breadcrumb/title) and global actions in AppShellHeader, and view-scoped controls (filters, saved views, bulk actions) in AppShellToolbar — don't crowd both into one bar. Let the content area scroll independently of the chrome.
Best practices
- Use
appearance="framed"for the tinted-canvas inset look; keep at most three surface levels on screen (canvas → raised → overlay). - Put a real <nav> inside AppShellSidebar; place the AppShellTrigger in the header.
- Seed
defaultOpenfrom theapp-shell:sidebarcookie server-side to avoid a collapse flash. - Compose ordinary garn components inside — Card, Table, Breadcrumb — they render identically in framed and flush.
- Don't bake the float into leaf components (cards, tables, tiles) — geometry lives in the shell
appearance. - Don't add a 4th nested surface; the hierarchy collapses.
- Don't reach for shadow to create the float — tonal step + hairline is the point.
- Don't give the sidebar its own landmark role on top of the detail Aside — keep one complementary region.
Accessibility
- Focus
- Skip-link reveals on focus and targets Main (tabIndex -1); focus-visible rings on controls are preserved.
Keyboard
| MetaB | Toggle the sidebar (Ctrl+B on Windows/Linux). |
| MetaK | Open the command palette, when AppShellCommand is used (Ctrl+K). |
| Tab | Skip link (first stop) jumps focus to the main region. |
- • AppShell renders a visually-hidden skip-link to the main region (its id defaults to `app-shell-main`; override `mainId` + the Main `id` together).
- • Landmark roles: AppShellHeader=<header>, AppShellMain=<main>, AppShellFooter=<footer>, AppShellAside=<aside> (complementary). The sidebar is a layout container — its inner <nav> is the navigation landmark — so the detail Aside stays the page's single complementary region.
- • AppShellTrigger exposes aria-expanded; give it (or rely on its default) an accessible name. AppShellAsideTrigger's aria-expanded reflects the effective aside visibility (inline pane or compact overlay).
- • AppShellNav is a <nav> landmark: name it with `aria-label`, or pass a `label` heading and it wires `aria-labelledby` to that heading automatically. AppShellNavItem sets aria-current="page" when `active`; an icon-only rail keeps the accessible name from the (visually hidden on rail) label.
- • At the compact tier the sidebar AND the detail aside render inside Sheets/dialogs with visually-hidden titles; the aside's overlay uses a separate open state so it doesn't auto-open, while a controlled Peek still drives it.
- Status
- Beta
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.