<!-- Protocore Design System — AppShell -->
# AppShell

- **Category:** Layout (`layout`)
- **Slug:** `layout/app-shell`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { AppShell } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/layout/app-shell

> The application frame — an optional env strip and top bar over a sidebar + main body row, with an optional footer and a responsive mobile drawer.

## When to use it

`AppShell` is the outermost frame for an **application** — a console or dashboard with persistent chrome. It owns the hard parts: the sticky env/top-bar stack, the sidebar-plus-main grid, and the responsive collapse to a drawer. Use it once, at the root of an app; compose the slots from the layout primitives (`TopBar`, `Sidebar`, `Footer`, `EnvStrip` — pass `env` and it renders the strip for you). It is not for marketing pages, which want a plain `TopBar` + `Section`s + `Footer` and no fixed rail. Keep sidebar open-state controlled at the app root so a menu button in the top bar and the backdrop stay in sync.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `children` | `ReactNode` | no | — | Main content region. |
| `className` | `string` | no | — | — |
| `defaultSidebarOpen` | `boolean` | no | `false` | Uncontrolled initial mobile sidebar open state. Default false. |
| `env` | `enum` | no | — | When set, renders a sticky environment strip above the top bar. |
| `envLabel` | `ReactNode` | no | — | Override the environment strip label. |
| `envMessage` | `ReactNode` | no | — | Wayfinding message for the environment strip (grows it into its loud form). |
| `footer` | `ReactNode` | no | — | The footer slot (e.g. a `<Footer>`), below the body. |
| `onSidebarOpenChange` | `((open: boolean) => void)` | no | — | Fires with the next open state when the mobile sidebar is toggled (e.g. by the click-away backdrop). |
| `sidebar` | `ReactNode` | no | — | The sidebar slot (e.g. a `<Sidebar>`). A fixed column on desktop; an off-canvas drawer on mobile. |
| `sidebarOpen` | `boolean` | no | — | Controlled mobile sidebar open state. Pair with `onSidebarOpenChange`. |
| `style` | `CSSProperties` | no | — | — |
| `topBar` | `ReactNode` | no | — | The top bar slot (e.g. a `<TopBar>`), pinned above the body. |

## Examples

### Basics

Fill the slots — `env`, `topBar`, `sidebar`, `footer`, and `children` for the main region. `AppShell` lays them out as a grid: env strip and top bar pinned on top, sidebar as a fixed column beside the main content, footer below.

```tsx
import {
  AppShell,
  TopBar,
  Sidebar,
  Footer,
  Logo,
  Button,
  Heading,
  Text,
  Stack,
} from "@protocore/pds";

export default function AppShellBasics() {
  return (
    <div style={{ height: 480, border: "1px solid var(--pds-border-faint)", overflow: "hidden" }}>
      <AppShell
        env="staging"
        topBar={
          <TopBar
            brand={<Logo label="PROTOCORE" size="sm" />}
            actions={
              <Button variant="secondary" size="sm">
                Console
              </Button>
            }
          />
        }
        sidebar={
          <Sidebar.Root aria-label="Console navigation">
            <Sidebar.Group>
              <Sidebar.GroupLabel>Overview</Sidebar.GroupLabel>
              <Sidebar.Item href="#" active>
                Dashboard
              </Sidebar.Item>
              <Sidebar.Item href="#">Nodes</Sidebar.Item>
              <Sidebar.Item href="#">Ledger</Sidebar.Item>
            </Sidebar.Group>
          </Sidebar.Root>
        }
        footer={
          <Footer
            columns={[{ heading: "PROTOCORE", links: ["Docs", "Status", "Support"] }]}
            copyright="© PROTOCORE 2026"
          />
        }
      >
        <Stack gap={3} style={{ padding: "var(--pds-space-6)" }}>
          <Heading size="h2">Dashboard</Heading>
          <Text color="secondary">
            AppShell wires the env strip, top bar, sidebar, main region, and footer into one frame.
            On desktop the sidebar is a grid column; below 860px it collapses to a drawer.
          </Text>
        </Stack>
      </AppShell>
    </div>
  );
}
```

### Mobile drawer

Below the 860px brand breakpoint the sidebar becomes an off-canvas drawer. Control it with `sidebarOpen` / `onSidebarOpenChange` and wire a menu button in the top bar to toggle it; the click-away backdrop closes it.

```tsx
import { useState } from "react";
import { AppShell, TopBar, Sidebar, Logo, IconButton, Heading, Text, Stack } from "@protocore/pds";
import { Menu } from "lucide-react";

export default function AppShellMobileDrawer() {
  const [open, setOpen] = useState(false);

  return (
    <div style={{ height: 420, border: "1px solid var(--pds-border-faint)", overflow: "hidden" }}>
      <AppShell
        sidebarOpen={open}
        onSidebarOpenChange={setOpen}
        topBar={
          <TopBar
            brand={<Logo label="PROTOCORE" size="sm" />}
            actions={
              <IconButton
                aria-label="Toggle navigation"
                variant="ghost"
                size="sm"
                onClick={() => setOpen((v) => !v)}
              >
                <Menu size={16} />
              </IconButton>
            }
          />
        }
        sidebar={
          <Sidebar.Root aria-label="Console navigation">
            <Sidebar.Group>
              <Sidebar.GroupLabel>Menu</Sidebar.GroupLabel>
              <Sidebar.Item href="#" active>
                Dashboard
              </Sidebar.Item>
              <Sidebar.Item href="#">Nodes</Sidebar.Item>
              <Sidebar.Item href="#">Ledger</Sidebar.Item>
            </Sidebar.Group>
          </Sidebar.Root>
        }
      >
        <Stack gap={3} style={{ padding: "var(--pds-space-6)" }}>
          <Heading size="h3">Controlled drawer</Heading>
          <Text color="secondary">
            The menu button toggles `sidebarOpen`; the backdrop closes it via `onSidebarOpenChange`.
            Below 860px the sidebar is an off-canvas drawer.
          </Text>
        </Stack>
      </AppShell>
    </div>
  );
}
```

## Do & don't

**Do**

- Use one AppShell at the root of an application to own its chrome.
- Pass env so the strip renders above the top bar automatically.
- Control sidebarOpen at the root and wire a top-bar menu button to it.

**Don't**

- Wrap marketing pages in AppShell — they want a plain TopBar and Sections.
- Nest AppShells or hand-build the sidebar/main grid yourself.
- Leave the mobile drawer uncontrolled if a top-bar button must also toggle it.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move through the top bar, sidebar, main, and footer in DOM order |
| `Enter / Space` | Activate the focused control (e.g. the menu toggle) |
| `Esc` | Not handled by AppShell — wire it to onSidebarOpenChange(false) if you want Esc to close the drawer |

**Notes**

- The frame renders semantic landmarks: `<main>` for content and `<aside>` for the sidebar column.
- The mobile backdrop is `aria-hidden` and closes the drawer on click.
- Give a top-bar menu toggle an `aria-label` and reflect the drawer state with `aria-expanded` on your button.

## Related

`top-bar`, `sidebar`, `footer`, `env-strip`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
