AppShell
Templatebeta@macaw-ds/ui · since 0.1.0The application frame: skip link, sticky TopBar, full-height SideNav column and the main area. One per app, at the root layout.
Import
import { AppShell } from '@macaw-ds/ui';Props
| Prop | Type | Default | Description |
|---|---|---|---|
| topBar* | ReactNode | — | The TopBar. |
| sideNav | ReactNode | — | The SideNav. Omit for pages without primary navigation. |
| sideNavLabel | string | Primary | Accessible name for the side column landmark. |
| mainId | string | main | id of the main element; the skip link targets it. |
| mainLabel | string | — | Accessible name for main. Only when several apps share one landmark list, such as an app embedded in an iframe. |
| skipLinkLabel | string | Skip to main content | Text of the skip link. |
| children | ReactNode | — | The page: usually PageHeader followed by content. |
Slots
Named parts you pass content into.
- topBar
- sideNav
- children
Tokens used
Every value this component renders comes from one of these tokens.
| Token | Light | Dark |
|---|---|---|
| color.surface.canvas semantic · color | oklch(0.985 0.002 250) | oklch(0.11 0.01 250) |
| color.surface.layer-1 semantic · color | oklch(1 0 0) | oklch(0.15 0.012 250) |
| color.line.default semantic · color | oklch(0.925 0.005 250) | oklch(0.29 0.012 250) |
| size.topbar semantic · dimension | 56px | 56px |
| size.sidenav semantic · dimension | 240px | 240px |
| z.sticky primitive · number | 100 | 100 |
| color.focus.ring semantic · color | oklch(0.51 0.2 262) | oklch(0.68 0.16 262) |
Eject
Ejecting copies the source into your repo and records it in .macaw/ejected.json, so the fork stays visible. The same file is served as a shadcn-compatible registry item.
npx @macaw-ds/cli add app-shell --ejectnpx shadcn@latest add https://macaw-design-system.vercel.app/r/app-shell.jsonBundle budget: 1,500 bytes gzipped, enforced by size-limit in CI.
Manifest
registry/manifests/app-shell.json, the contract the playground, CLI, plugin and MCP server read.
{
"name": "AppShell",
"slug": "app-shell",
"package": "@macaw-ds/ui",
"status": "beta",
"since": "0.1.0",
"level": "template",
"category": "layout",
"purpose": "The application frame: skip link, sticky TopBar, full-height SideNav column and the main area. One per app, at the root layout.",
"description": "Lays out the product chrome with depth by tone: the canvas is the background, the TopBar and SideNav column sit on layer-1. The SideNav column is sticky, exactly the viewport height below the TopBar, and scrolls on its own; the window scrolls the main area. A skip link is the first Tab stop.",
"exports": [
"AppShell"
],
"props": {
"topBar": {
"type": "node",
"required": true,
"description": "The TopBar.",
"control": "none"
},
"sideNav": {
"type": "node",
"required": false,
"description": "The SideNav. Omit for pages without primary navigation.",
"control": "none"
},
"sideNavLabel": {
"type": "string",
"default": "Primary",
"required": false,
"description": "Accessible name for the side column landmark.",
"control": "none"
},
"mainId": {
"type": "string",
"default": "main",
"required": false,
"description": "id of the main element; the skip link targets it.",
"control": "none"
},
"mainLabel": {
"type": "string",
"required": false,
"description": "Accessible name for main. Only when several apps share one landmark list, such as an app embedded in an iframe.",
"control": "none"
},
"skipLinkLabel": {
"type": "string",
"default": "Skip to main content",
"required": false,
"description": "Text of the skip link.",
"control": "none"
},
"children": {
"type": "node",
"required": false,
"description": "The page: usually PageHeader followed by content.",
"control": "none"
}
},
"slots": [
"topBar",
"sideNav",
"children"
],
"tokens": [
"color.surface.canvas",
"color.surface.layer-1",
"color.line.default",
"size.topbar",
"size.sidenav",
"z.sticky",
"color.focus.ring"
],
"a11y": {
"role": "Provides the landmarks: banner (TopBar), complementary (side column, named), main",
"focusable": true,
"keyboard": [
"Tab: the first stop is the skip link; Enter moves focus to main"
],
"name": "The side column is named by sideNavLabel; main and banner are unique per page.",
"notes": [
"The skip link satisfies WCAG 2.4.1 Bypass Blocks; it is visually hidden until focused.",
"main has tabIndex=-1 so the skip link can move focus to it.",
"Sticky chrome is offset with size.topbar so focused content is never obscured (WCAG 2.2 SC 2.4.11).",
"Below 1024px nothing is sticky and the side column stacks above main, so the page reflows at 320px wide (WCAG 1.4.10). At desktop widths, scroll padding keeps focused elements clear of the sticky chrome (WCAG 2.4.11).",
"Screen readers list an iframe's landmarks with the host page's; an embedded app should name its banner (TopBar aria-label) and main (mainLabel) so landmarks stay unique."
]
},
"budget": {
"gzip": 1500
},
"rules": [
"One AppShell per application, at the root layout.",
"Put a TopBar in topBar and a SideNav in sideNav; do not build the frame by hand.",
"Start each page with a PageHeader, then the content in a px-margin py-section column."
],
"dos": [
"Set data-density=\"compact\" on html (or the AppShell) for data-dense products; the frame adapts."
],
"donts": [
"Do not add a second scroll container around the page; the window scrolls main.",
"Do not give the main area its own background; it sits on the canvas."
],
"examples": [
{
"name": "Default",
"props": {}
}
],
"registryDependencies": [
"top-bar",
"side-nav"
],
"dependencies": [],
"related": [
"top-bar",
"side-nav",
"page-header"
]
}