AppShell is the top-level layout for application-style UIs. It owns the grid (sidebar + header over main), the show/hide state for the sidebar, and the desktop/mobile mode switch. Children compose the sidebar regions, header, and main content area.

Below the configured mobile breakpoint the sidebar transforms into a popover-driven off-canvas drawer, and the header auto-injects a hamburger toggle. The same <aside> element serves both modes — no duplicate DOM trees.

Usage

On desktop the sidebar toggles between fully visible and fully hidden — there is no icon-rail intermediate state. Below the mobile breakpoint it slides in as an off-canvas drawer.

<div class="zui-app-shell" data-zui-app-shell>
<a class="zui-app-shell-skip-link" href="#main">Skip to main content</a>

<aside class="zui-app-shell-sidebar" popover="auto" aria-label="Sidebar">
  <div class="zui-app-shell-sidebar-header">
    <i class="ph ph-sparkle" aria-hidden="true"></i>
    <span>Acme</span>
  </div>
  <div class="zui-app-shell-sidebar-body">
    <a href="#"><i class="ph ph-house"></i><span>Home</span></a>
  </div>
  <div class="zui-app-shell-sidebar-footer">
    <i class="ph ph-user-circle"></i>
    <span>Account</span>
  </div>
</aside>

<header class="zui-app-shell-header">
  <button type="button" class="zui-app-shell-toggle" aria-label="Toggle sidebar">
    <i class="ph ph-list" aria-hidden="true"></i>
  </button>
  <strong>Dashboard</strong>
</header>

<main id="main" class="zui-app-shell-main">
  <h1>Welcome</h1>
</main>
</div>

For a richer standalone example with real content, see /app-shell-example.

Anatomy

AppShell
├── AppShellSidebar
│   ├── AppShellSidebarHeader   (pinned, height = header)
│   ├── AppShellSidebarBody     (scrolls)
│   └── AppShellSidebarFooter   (pinned)
├── AppShellHeader              (over main only — sidebar is full height)
└── AppShellMain                (scrolls)

Props — AppShell

PropTypeDefaultDescription
position'left' | 'right''left'Side the sidebar appears on.
defaultCollapsedbooleanfalseInitial collapsed state (uncontrolled).
collapsedbooleanControlled collapsed state.
onCollapsedChange(collapsed: boolean) => voidFired when collapse changes.
mobileBreakpointnumber768Container width (px) below which mobile/drawer mode activates.
storageKeystring | null'zui-app-shell-collapsed'localStorage key. Pass null to disable persistence.
shortcutbooleanfalseWhen true, binds Cmd/Ctrl + B to toggle the sidebar.

Props — AppShellHeader

PropTypeDefaultDescription
togglebooleantrueAuto-inject the sidebar toggle button.
toggleLabelstring'Toggle sidebar'aria-label for the toggle button.

Props — AppShellSidebar

PropTypeDefaultDescription
labelstring'Sidebar'aria-label for the navigation landmark.

Mobile mode

Below the configured mobileBreakpoint (default 768px):

The mobile breakpoint is enforced by a viewport @media query. AppShell is designed to occupy the full viewport — embedding it inside a narrower parent will not switch to mobile mode unless the viewport itself is below the breakpoint.

Persistence and no-flash hydration

When storageKey is set (default), the collapse state is persisted to localStorage. The Astro wrapper ships an inline no-flash script that applies data-collapsed before the controller boots, preventing layout flicker on first paint. For other frameworks, import and inline appShellNoFlashScript(storageKey?) from @mrmartineau/zui/<framework>:

<script>{appShellNoFlashScript()}</script>

Tokens

All sizing tokens are scoped to the .zui-app-shell root and prefixed --zui-app-shell-*. Override at the call site:

.my-app .zui-app-shell {
  --zui-app-shell-sidebar-width: 17rem;
  --zui-app-shell-header-height: 3.5rem;
  --zui-app-shell-main-padding: var(--space-lg);
}

To change the mobile breakpoint, re-author the @media (max-width: ...) rule in your own stylesheet — @media cannot read CSS custom properties.

Keyboard

Accessibility

Theme

Copy this CSS to your project: