Repo

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

Prop Type Default Description
position 'left' | 'right' 'left' Side the sidebar appears on.
defaultCollapsed boolean false Initial collapsed state (uncontrolled).
collapsed boolean — Controlled collapsed state.
onCollapsedChange (collapsed: boolean) => void — Fired when collapse changes.
mobileBreakpoint number 768 Container width (px) below which mobile/drawer mode activates.
storageKey string | null 'zui-app-shell-collapsed' localStorage key. Pass null to disable persistence.
shortcut boolean false When true, binds Cmd/Ctrl + B to toggle the sidebar.

Props — AppShellHeader

Prop Type Default Description
toggle boolean true Auto-inject the sidebar toggle button.
toggleLabel string 'Toggle sidebar' aria-label and tooltip text for the toggle button.

Props — AppShellSidebar

Prop Type Default Description
label string '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>

CSS custom properties

Set these on :root, on a wrapper, or on the element. See Component custom properties.

Property Default Description
--zui-app-shell-sidebar-width 15rem Sidebar width on desktop
--zui-app-shell-header-height 3rem Header height
--zui-app-shell-main-padding var(--space-md) Padding inside the main area
--zui-app-shell-transition-duration 200ms Duration of the sidebar collapse
--zui-app-shell-bg var(--color-background) Background colour
--zui-app-shell-surface var(--color-surface) Sidebar and header background
--zui-app-shell-border var(--color-border) Border colour
--zui-app-shell-sidebar-padding var(--space-xs) Padding inside the sidebar body
--zui-app-shell-drawer-width min(20rem, 85vw) Sidebar width on mobile
--zui-app-shell-toggle-size 2rem Width and height of the toggle button
--zui-app-shell-toggle-radius var(--radius-md) Border radius of the toggle button
--zui-app-shell-toggle-font-size var(--step-1) Icon size in the toggle button

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

Keyboard

Toggle tooltip

Hovering or keyboard-focusing the header toggle shows a tooltip with the toggleLabel text. When shortcut is enabled, the tooltip also shows the shortcut: ⌘ B on Apple devices, Ctrl B elsewhere. Esc hides it.

The framework wrappers render the tooltip for you. In plain HTML, put this markup directly after the toggle button. The controller shows and hides it. The stylesheet shows ⌘ or Ctrl in the empty .zui-app-shell-toggle-mod key, so leave that element empty:

<span
  popover="manual"
  aria-hidden="true"
  class="zui-tooltip-content zui-tooltip-placement-bottom zui-app-shell-toggle-tooltip"
>
  Toggle sidebar
  <kbd class="zui-kbd-group zui-app-shell-toggle-kbd">
    <kbd class="zui-kbd zui-app-shell-toggle-mod"></kbd>
    <kbd class="zui-kbd">B</kbd>
  </kbd>
</span>

Accessibility

Theme

Copy this CSS to your project: