AppShell
The page's frame. The Sidebar and the TopBar on screens 640 px and wider, the MobileTopBar and the MobileTabBar below that, around the page's main content, switched by CSS with no script.
Import
import { AppShell } from '@nizivo/react/app-shell';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/mobile-tab-bar.css and @nizivo/react/css/mobile-top-bar.css and @nizivo/react/css/sidebar.css and @nizivo/react/css/top-bar.css
import '@nizivo/react/css/app-shell.css';Static or interactive
Server. AppShell places the parts it is given and ships no client JavaScript of its own. The parts bring their own: the SidebarItems' Tooltip, the MobileTabBar's island and More sheet, and whatever the bars hold. How to choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
Slots
sidebar: aSidebar, shown from 640 px.topBar: aTopBar, shown from 640 px.mobileTopBar: aMobileTopBar, shown below 640 px.tabBar: aMobileTabBar, fixed to the bottom below 640 px.children: the page, inside the<main>AppShell renders.
The server renders both sets of bars, and CSS shows one: the hidden one is display: none, so it is
out of the accessibility tree and its links are not Tab stops. In Next.js, render AppShell in a
layout, so it stays mounted while the pages change.
Main and the skip link
AppShell renders the page's one <main> (with mainId, or a generated id) and a skip link to it,
the first Tab stop, shown only while focused ("Skip to content", labels.skip). Don't render
another <main> in the page. The page is never wider than layout/content-max, with the grid's
margin around it, and below 640 px it leaves room for the tab bar.
Layout
From 640 px, a two-column grid: the Sidebar's own width (the 72 px rail, or 264 px from 1280 px)
beside the bar and the page. Below 640 px, one column, with the tab bar fixed to the bottom in the
--nz-z-chrome band.
Hooks: --nz-app-shell-bg, --nz-app-shell-main-padding.
Examples
Basic
The shell of Figma's 08 pages: the Sidebar (the icon rail from 640 px, 264 px from 1280 px) and the TopBar over the page; below 640 px the MobileTopBar and the MobileTabBar with its More sheet. The server renders both sets and CSS shows one. AppShell owns <main> and the skip link to it, the first Tab stop. (The example sets a height; in an app the shell fills the viewport.)
Good morning, Maya
import type { JSX, ReactNode } from 'react';
import { Avatar } from '@nizivo/react/avatar';
import { IconButton } from '@nizivo/react/icon-button';
import { MobileTabBar, MobileTabBarMore } from '@nizivo/react/mobile-tab-bar';
import { MobileTopBar } from '@nizivo/react/mobile-top-bar';
import { NavItem } from '@nizivo/react/nav-item';
import { PageHeader } from '@nizivo/react/page-header';
import { SearchField } from '@nizivo/react/search-field';
import { Sidebar, SidebarItem } from '@nizivo/react/sidebar';
import { TopBar } from '@nizivo/react/top-bar';
import { AppShell } from '@nizivo/react/app-shell';
/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
);
}
const ICONS = {
home: (
<Glyph>
<path d="M3 10.5 12 3l9 7.5V21H3z" />
<path d="M9 21v-7h6v7" />
</Glyph>
),
orders: (
<Glyph>
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
</Glyph>
),
products: (
<Glyph>
<path d="m21 8-9-5-9 5v8l9 5 9-5z" />
<path d="m3 8 9 5 9-5M12 13v8" />
</Glyph>
),
customers: (
<Glyph>
<circle cx="9" cy="8" r="4" />
<path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
</Glyph>
),
settings: (
<Glyph>
<circle cx="12" cy="12" r="3" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
</Glyph>
),
search: (
<Glyph>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</Glyph>
),
bell: (
<Glyph>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0M3.26 15.33A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.41 13.96 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.41 5.96-2.74 7.33" />
</Glyph>
),
};
export function AppShellExample(): JSX.Element {
return (
<AppShell
style={{ minBlockSize: 480 }}
sidebar={
<Sidebar.Root style={{ blockSize: 480 }}>
<Sidebar.Header mark="M" title="Moss & Kiln" description="mossandkiln.com" />
<Sidebar.Content>
<SidebarItem href="/" icon={ICONS.home} current>
Home
</SidebarItem>
<SidebarItem href="/orders" icon={ICONS.orders} count={12}>
Orders
</SidebarItem>
<SidebarItem href="/products" icon={ICONS.products}>
Products
</SidebarItem>
<SidebarItem href="/customers" icon={ICONS.customers}>
Customers
</SidebarItem>
</Sidebar.Content>
<Sidebar.Footer>
<SidebarItem href="/settings" icon={ICONS.settings}>
Settings
</SidebarItem>
</Sidebar.Footer>
</Sidebar.Root>
}
topBar={
<TopBar
title="Overview"
search={
<form method="get" action="/search">
<SearchField
name="q"
placeholder="Search orders, products, customers"
labels={{ label: 'Search the store' }}
/>
</form>
}
actions={
<>
<IconButton icon={ICONS.bell} label="Notifications" variant="secondary" />
<Avatar name="Maya Lindqvist" />
</>
}
/>
}
mobileTopBar={
<MobileTopBar
mark="M"
title="Overview"
actions={
<>
<IconButton icon={ICONS.search} label="Search" variant="tertiary" />
<IconButton icon={ICONS.bell} label="Notifications" variant="tertiary" />
</>
}
/>
}
tabBar={
<MobileTabBar.Root>
<MobileTabBar.Item href="/" icon={ICONS.home} current>
Home
</MobileTabBar.Item>
<MobileTabBar.Item href="/orders" icon={ICONS.orders} count={12}>
Orders
</MobileTabBar.Item>
<MobileTabBar.Item href="/products" icon={ICONS.products}>
Products
</MobileTabBar.Item>
<MobileTabBar.Item href="/customers" icon={ICONS.customers}>
Customers
</MobileTabBar.Item>
<MobileTabBarMore>
<NavItem href="/settings" icon={ICONS.settings}>
Settings
</NavItem>
</MobileTabBarMore>
</MobileTabBar.Root>
}
>
<PageHeader title="Good morning, Maya" />
</AppShell>
);
}
Props
Server component Renders data-slot="app-shell" with the class nz-app-shell. Props: AppShellProps. Source: src/app-shell/app-shell.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The page, inside |
labels | Partial<AppShellLabels> | – | |
mainId | string | – | The |
mobileTopBar | ReactNode | – | A |
sidebar | ReactNode | – | A |
tabBar | ReactNode | – | A |
topBar | ReactNode | – | A |
- Also takes every attribute of
<div>exceptchildren. (Omit<ComponentPropsWithRef<"div">, "children">)
Other exports
AppShellLabelstypeimport type { AppShellLabels } from '@nizivo/react/app-shell'Strings this part renders (§16.4).
Name Type Default Description skiprequiredstring'Skip to content'The skip link, the first Tab stop.
AppShellPropstypeimport type { AppShellProps } from '@nizivo/react/app-shell'The props of
AppShell, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
app-shell.nz-app-shell | AppShellroot (no render) | none |
app-shell-frame.nz-app-shell-frame | AppShellinner element (CSS only) | none |
app-shell-main.nz-app-shell-main | AppShellinner element (CSS only) | none |
app-shell-mobile-top-bar.nz-app-shell-mobile-top-bar | AppShellinner element (CSS only) | none |
app-shell-sidebar.nz-app-shell-sidebar | AppShellinner element (CSS only) | none |
app-shell-skip.nz-app-shell-skip | AppShellinner element (CSS only); interactive, focus: self | none |
app-shell-tab-bar.nz-app-shell-tab-bar | AppShellinner element (CSS only) | none |
app-shell-top-bar.nz-app-shell-top-bar | AppShellinner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-app-shell-bg | --nz-bg-canvas |
|
--nz-app-shell-main-padding | var(--nz-section-gap) var(--nz-grid-margin) |
|
Layers: nz.components. Classes: .nz-app-shell, .nz-app-shell-frame, .nz-app-shell-main, .nz-app-shell-mobile-top-bar, .nz-app-shell-sidebar, .nz-app-shell-skip, .nz-app-shell-tab-bar, .nz-app-shell-top-bar. Runtime variables: --nz-layout-bottom-inset.
Tokens this stylesheet reads (15)
--nz-bg-canvas, --nz-bg-inverse, --nz-font-label-m-strong, --nz-grid-margin, --nz-layout-content-max, --nz-radius-md, --nz-section-gap, --nz-space-2, --nz-space-3, --nz-space-4, --nz-text-inverse, --nz-text-primary, --nz-tracking-label-m-strong, --nz-z-chrome, --nz-z-overlay
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
app-shell-skip: draws the focus outline itself on keyboard focus.
Keyboard
| Keys | Action |
|---|---|
| Tab | From the top of the page, the first stop is the skip link, shown while focused. Then the sidebar, the bar and the page, in document order. |
| Enter | On the skip link, moves focus to the page's main content. |
Source: a native skip link to <main tabindex="-1"> (app-shell.tsx, no client code). The browser lane (test/browser/app-shell.spec.tsx) checks the skip link, the parts shown at 1280, 960 and 390 px, and the bottom inset, in Chromium, Firefox and WebKit.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: none
- Ghost masters: none
Alert
A tinted message with a title, an optional action and an optional dismiss button. Static and server-rendered unless you add the dismiss leaf.
Avatar
A person's or store's photo with an initials fallback that needs no script, and AvatarGroup, a stack of avatars and a count announced as one item ("Maya, Jonah, Priya, and 4 more").