Skip to content

AppShell

The frame of an app: a header with the brand, the context and the account, a navigation beside the page on a wide screen and folded into a menu on a narrow one, and the main landmark, reached first by a skip link.

import { AppShell } from '@zitdevs/ui';

The demo is server HTML with no React on the page, which runs one script, enhanceAppShellMenus(document) from @zitdevs/ui/enhance, exactly as a site without React would. Narrow the window below 1024px, open the menu, and press Escape: it closes and focus goes back to the toggle. Press Tab once from the top of the page to meet the skip link.

Skip to content
ZitStack
Development
Menu

People

Synced

Everyone in the organization.

<AppShell
brand={<Logo />}
context={<Tag label="Development" />}
account={<Button label="Ada Lovelace" variant="ghost" size="sm" />}
nav={<SideNav id="app-nav" label="Main" groups={groups} />}
>
<PageHeader title="People" />
{/* the page */}
</AppShell>

In Next.js it works from a Server Component in app/layout.tsx; only the menu’s small client part ships to the browser.

Prop Type Default Notes
brand ReactNode required The product’s mark and name, at the start of the header.
nav ReactNode required The navigation, a SideNav.
context ReactNode — Where the reader is working, on every page: an environment, an organization.
account ReactNode — Who is signed in, at the end of the header.
children ReactNode — The page. Start it with a PageHeader.
mainId string 'main' The id of the main landmark, which the skip link names.
labels Partial<AppShellLabels> English { skipToContent, menu }. es.appShell from labels/es.
ref Ref<HTMLDivElement> — The shell’s root.
className string — Placement; the shell is a viewport tall by default.
anything else <div> attributes —

Exported types: AppShellProps, AppShellLabels, and the label sets appShellLabels. From @zitdevs/ui/enhance: enhanceAppShellMenus.

  • Landmarks: a <header> (the banner), the navigation, and <main>. A PageHeader inside the page is a header of the page, not a second banner.
  • The skip link is the first thing a keyboard reaches and shows when it has focus. It names the <main>, which takes focus (tabindex="-1"), so the next Tab starts in the page.
  • The menu’s toggle is a <summary>, so Enter and Space operate it. The enhancer mirrors the open state as aria-expanded, closes on Escape and gives focus back to the toggle, and closes when a link in it is followed.
  • From the lg width up the navigation is a sidebar: the enhancer holds it open and the toggle goes away. Escape and links leave it alone there.
  • The environment shows in the header on every page, as text.

The server renders the menu closed, in a <details>, and a <details> opens and closes with no script: the toggle is reachable at every width, the navigation behind it. Without the enhancer the toggle stays at desktop width too, so the navigation is never out of reach. What the enhancer adds is the Escape key with focus restored, aria-expanded, closing on a followed link, and the open sidebar at desktop width:

  • React: nothing to do. The menu is a small client part that runs the enhancer once it has hydrated.
  • No React: call enhanceAppShellMenus(document) once. It finds every details[data-app-shell-menu] and enhances each once.

surface for the page and surface-raised for the header and the menu; border-default for the lines; ink, muted and body for text; the focus ring. A product re-themes the shell by overriding the roles.