App shell
The frame of every screen, holding the header, the navigation, the page, a side panel and a footer, laid out by its own width.
On this page
Code
import { AlertListRow } from "@bosunkit/components/alert-list-row";
import { AlertZone, type AlertZoneAlert } from "@bosunkit/components/alert-zone";
import { AppShell } from "@bosunkit/components/app-shell";
import { DescriptionList } from "@bosunkit/components/description-list";
import { HeaderBar } from "@bosunkit/components/header-bar";
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { StatusStrip, type SensorSummary } from "@bosunkit/components/status-strip";
import { NavigationBar, type NavigationBarItem } from "@bosunkit/components/navigation-bar";
import { TelemetryStrip } from "@bosunkit/components/telemetry-strip";
import {
Anchor,
BookOpen,
Cloud,
House,
LayoutDashboard,
Map as MapIcon,
Menu,
MessageSquare,
Settings,
TriangleAlert,
Users,
Waves,
} from "lucide-react";
import { useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The app shell at anchor in Morgat, at a phone's, a tablet's and a desktop's width: the header bar with the " +
"anchor drag alarm, the navigation as a tab bar, a rail or a panel, the alerts, the side panel under or beside " +
"them, and splitters on a desktop whose widths the browser keeps.";
export const readsClock: boolean = true;
const WIDTHS: readonly SegmentedRadioOption[] = [
{ value: "24em", label: "Phone" },
{ value: "45em", label: "Tablet" },
{ value: "80em", label: "Desktop" },
];
const GROUPS: readonly NavGroup[] = [
{
id: "underway",
label: "Underway",
items: [
{ href: "#dashboard", label: "Dashboard", icon: <LayoutDashboard /> },
{
href: "#situation",
label: "Situation",
icon: <TriangleAlert />,
count: { value: 1, label: "1 unacknowledged alert", priority: "alarm" },
},
{ href: "#chart", label: "Chart", icon: <MapIcon />, count: { value: 6, label: "6 targets tracked" } },
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
],
},
{
id: "passage",
label: "Passage",
items: [
{ href: "#weather", label: "Weather", icon: <Cloud /> },
{ href: "#tides", label: "Tides", icon: <Waves /> },
{ href: "#log", label: "Log", icon: <BookOpen /> },
],
},
{
id: "aboard",
label: "Aboard",
items: [
{ href: "#chat", label: "Chat", icon: <MessageSquare /> },
{ href: "#crew", label: "Crew", icon: <Users />, count: { value: 4, label: "4 people aboard" } },
{ href: "#settings", label: "Settings", icon: <Settings /> },
],
},
];
const SENSORS: readonly SensorSummary[] = ["gps", "compass", "depth", "wind"].map((name: string): SensorSummary => ({
name,
integrity: "fresh",
ageSeconds: 1,
}));
function ignore(): void {
// The example changes nothing on these taps.
}
/**
* The shell at the chosen width in a frame that scrolls sideways. On a declared screen (a device preview, or the hero
* narrowed) the screen is the width: the switch hides, the shell takes the screen's width, and the frame scrolls
* nothing, so the tab bar stays at the foot of the screen.
*/
const FRAME_STYLES: string = `
.app-shell-showcase__frame {
width: 100%;
overflow-x: auto;
border: 1px solid var(--border);
}
.app-shell-showcase__screen {
width: var(--app-shell-showcase-width);
}
@container bk-screen (min-width: 0px) {
.app-shell-showcase__width {
display: none;
}
.app-shell-showcase__frame {
overflow-x: visible;
}
.app-shell-showcase__screen {
width: auto;
}
}
`;
export default function Showcase(): ReactElement {
const [width, setWidth]: [string, Dispatch<SetStateAction<string>>] = useState<string>("45em");
const [menuOpen, setMenuOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
const [openedAt]: [number, Dispatch<SetStateAction<number>>] = useState<number>((): number => Date.now());
const [acknowledged, setAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
function acknowledge(): Promise<void> {
// Stands in for the board: the alarm reads acknowledged once the next snapshot confirms it.
return new Promise<void>((resolve: () => void): void => {
setTimeout((): void => {
setAcknowledged(true);
resolve();
}, 1200);
});
}
const alarm: AlertZoneAlert | null = acknowledged
? null
: {
id: "drag",
title: "Anchor dragging",
priority: "alarm",
raisedAt: openedAt - 2 * 60_000,
};
const tabs: readonly NavigationBarItem[] = [
{ id: "home", label: "Home", icon: <House />, href: "#dashboard" },
{ id: "chart", label: "Chart", icon: <MapIcon />, href: "#chart" },
{
id: "alerts",
label: "Alerts",
icon: <TriangleAlert />,
href: "#situation",
...(acknowledged ? {} : { count: { value: 1, label: "1 unacknowledged alert", priority: "alarm" as const } }),
},
{ id: "chat", label: "Chat", icon: <MessageSquare />, href: "#chat" },
{
id: "more",
label: "More",
icon: <Menu />,
expanded: menuOpen,
onSelect: (): void => {
setMenuOpen(!menuOpen);
},
},
];
return (
<div style={{ display: "grid", gap: "var(--space-md)", width: "100%" }}>
<style href="bosunkit-example-app-shell" precedence="default">
{FRAME_STYLES}
</style>
<div className="app-shell-showcase__width">
<SegmentedRadioGroup label="Screen width" options={WIDTHS} value={width} onValueChange={setWidth} />
</div>
<div className="app-shell-showcase__frame">
<div className="app-shell-showcase__screen" style={{ "--app-shell-showcase-width": width } as CSSProperties}>
<AppShell
layoutStorageKey="bosun-design-app-shell"
menuOpen={menuOpen}
onMenuOpenChange={setMenuOpen}
header={
<HeaderBar
title="Situation"
alertZone={
<AlertZone
count={acknowledged ? 0 : 1}
alert={alarm}
acknowledgeAction={acknowledge}
boardHref="#situation"
/>
}
statusStrip={
<StatusStrip
connection="live"
lastEventAt={openedAt}
activity="anchored"
sensors={SENSORS}
risk={null}
doNotDisturb={{ enabled: false, missed: 0, onToggle: ignore }}
nightSuggested={false}
onDismissSuggestion={ignore}
unacknowledgedAlertIds={acknowledged ? [] : ["drag"]}
/>
}
/>
}
navigation={<NavList groups={GROUPS} currentHref="#situation" />}
navigationBar={<NavigationBar items={tabs} current="alerts" />}
main={
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<SectionHeading level={2} count={3}>
Active alerts
</SectionHeading>
<ul style={{ display: "grid", gap: "var(--space-xs)", margin: 0, padding: 0, listStyle: "none" }}>
<AlertListRow
priority="alarm"
title="Anchor dragging"
description="Althea is 41 m from the drop point, outside the 35 m swing radius."
source="Anchor watch"
time="02:16"
timeReference="UTC"
state={acknowledged ? "acknowledged" : "unacknowledged"}
acknowledgeAction={acknowledge}
/>
<AlertListRow
priority="warning"
title="Gusts above 25 kn"
source="Wind"
time="01:52"
timeReference="UTC"
state="acknowledged"
/>
<AlertListRow
priority="caution"
title="House bank at 48 %"
source="Battery monitor"
time="00:40"
timeReference="UTC"
state="unacknowledged"
/>
</ul>
</div>
}
aside={
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<SectionHeading level={2}>Anchor</SectionHeading>
<DescriptionList
items={[
{ term: "Dropped", value: "21:40 UTC" },
{ term: "Depth", value: "6.4 m" },
{ term: "Rode out", value: "35 m" },
{ term: "Swing radius", value: "35 m" },
]}
/>
</div>
}
footer={
<TelemetryStrip
groups={[
{ label: "House", value: "12.4 V", status: "warn" },
{ label: "Wind", value: "22 kn" },
{ label: "Depth", value: "6.4 m" },
]}
/>
}
/>
</div>
</div>
</div>
);
}
The app shell is the frame every screen sits in: the header bar at the top, the navigation at the start, the page in the
middle, a side panel at the end and a footer along the foot. It holds no data; each region is a slot that takes a kit
part, a HeaderBar, a NavList, a NavigationBar or the page's own content. ui/web keeps the routing: the links in
the navigation and the navigation bar go through the provider's link component, and the caller says which page is
current.
Each region is a landmark. The header is the page's banner, the navigation a navigation landmark named "All pages",
the page main, the side panel a complementary region named by asideLabel, and the footer contentinfo. A skip
link leads the tab order, out of sight until it takes focus, and moves focus to the page.
Layout
The shell's root is an inline-size container, so it lays out by its own width, not the window's. The widths are the kit's breakpoints in em, measured against the shell's font size, so a larger size class lays out as a narrower screen in which its larger targets fit.
| Shell width | Navigation | Side panel |
|---|---|---|
| Phone, up to 40em | The tab bar at the foot, or a menu button in the header that opens the navigation above the page | Under the page |
| Tablet, up to 68.75em | An icon rail, 4.5em wide | Under the page |
| Desktop, wider than 68.75em | A panel with labels, 14em wide, with a splitter | Beside the page, 20em wide, with a splitter |
The root is also the page's bk-screen, the container the kit's narrow forms query: the status strip drops its
captions, the alert zone takes its own row and buttons grow to a thumb's target when the shell is phone-wide, wherever
it is drawn. The header stays at the top of the window on the alert layer, so a dialog or the dimmer never covers the
alert zone; stickyHeader={false} lets a shell scroll with a document, as on this page. With fillHeight the shell
fills a container of fixed height (the window, or a device's screen) and the page scrolls inside it under the header;
it is then the bk-screen on both axes, so a component's @container bk-screen (max-height: …) query measures it.
Examples
A phone with a tab bar
Tides
High water at Morgat 14:12 UTC, 6.1 m.
Code
import { AppShell } from "@bosunkit/components/app-shell";
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { NavigationBar } from "@bosunkit/components/navigation-bar";
import { BookOpen, Cloud, House, Map as MapIcon, Menu, MessageSquare, Waves } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"At a phone's width the navigation gives way to the tab bar at the foot; its More opens every page above the " +
"content, and following a link closes it.";
const GROUPS: readonly NavGroup[] = [
{
id: "passage",
label: "Passage",
items: [
{ href: "#weather", label: "Weather", icon: <Cloud /> },
{ href: "#tides", label: "Tides", icon: <Waves /> },
{ href: "#log", label: "Log", icon: <BookOpen /> },
],
},
];
export default function PhoneWithANavigationBar(): ReactElement {
const [menuOpen, setMenuOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
return (
<div style={{ width: "24em", maxWidth: "100%", border: "1px solid var(--border)" }}>
<AppShell
stickyHeader={false}
menuOpen={menuOpen}
onMenuOpenChange={setMenuOpen}
header={<p style={{ margin: 0, padding: "var(--space-sm) var(--space-lg)" }}>Tides</p>}
navigation={<NavList groups={GROUPS} currentHref="#tides" />}
main={<p style={{ margin: 0 }}>High water at Morgat 14:12 UTC, 6.1 m.</p>}
navigationBar={
<NavigationBar
current="more"
items={[
{ id: "home", label: "Home", icon: <House />, href: "#dashboard" },
{ id: "chart", label: "Chart", icon: <MapIcon />, href: "#chart" },
{ id: "chat", label: "Chat", icon: <MessageSquare />, href: "#chat" },
{
id: "more",
label: "More",
icon: <Menu />,
expanded: menuOpen,
onSelect: (): void => {
setMenuOpen(!menuOpen);
},
},
]}
/>
}
/>
</div>
);
}
A phone with a menu
Anchor
Holding: 18 m from the drop point, inside the 35 m swing radius.
Code
import { AppShell } from "@bosunkit/components/app-shell";
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { Anchor, Cloud, LayoutDashboard } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"Without a tab bar, a phone-wide shell puts a menu button at the start of the header; it opens the navigation " +
"above the page, and Escape closes it.";
const GROUPS: readonly NavGroup[] = [
{
id: "pages",
items: [
{ href: "#dashboard", label: "Dashboard", icon: <LayoutDashboard /> },
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
{ href: "#weather", label: "Weather", icon: <Cloud /> },
],
},
];
export default function PhoneWithAMenu(): ReactElement {
return (
<div style={{ width: "24em", maxWidth: "100%", border: "1px solid var(--border)" }}>
<AppShell
stickyHeader={false}
header={<p style={{ margin: 0, padding: "var(--space-sm) var(--space-md)" }}>Anchor</p>}
navigation={<NavList groups={GROUPS} currentHref="#anchor" />}
main={<p style={{ margin: 0 }}>Holding: 18 m from the drop point, inside the 35 m swing radius.</p>}
/>
</div>
);
}
A tablet
Weather
Southwest 18 kn, gusts 24 kn, backing south by the evening.
Code
import { AppShell } from "@bosunkit/components/app-shell";
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { Anchor, Cloud, LayoutDashboard, TriangleAlert } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"At a tablet's width the navigation is an icon rail, each link named in a tooltip, and the side panel folds " +
"under the page.";
const GROUPS: readonly NavGroup[] = [
{
id: "underway",
label: "Underway",
items: [
{ href: "#dashboard", label: "Dashboard", icon: <LayoutDashboard /> },
{
href: "#situation",
label: "Situation",
icon: <TriangleAlert />,
count: { value: 2, label: "2 unacknowledged alerts", priority: "warning" },
},
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
{ href: "#weather", label: "Weather", icon: <Cloud /> },
],
},
];
export default function Tablet(): ReactElement {
return (
<div style={{ width: "45em", maxWidth: "100%", overflowX: "auto", border: "1px solid var(--border)" }}>
<div style={{ width: "45em" }}>
<AppShell
stickyHeader={false}
header={<p style={{ margin: 0, padding: "var(--space-sm) var(--space-lg)" }}>Weather</p>}
navigation={<NavList groups={GROUPS} currentHref="#weather" />}
main={<p style={{ margin: 0 }}>Southwest 18 kn, gusts 24 kn, backing south by the evening.</p>}
aside={<p style={{ margin: 0 }}>Barometer 1011 hPa, falling 3.1 hPa in 3 hr.</p>}
asideLabel="Barometer"
/>
</div>
</div>
);
}
Resizing on a desktop
The button above the navigation folds the panel to its rail and expands it to the last panel width. It stays in
the same navigation region and shares state with the splitter. It is available with resizable={false}; setting
collapsible={false} removes it. Fixed rail layouts and phone menus use their own presentation without this toggle.
Each splitter is a window splitter: a separator named for its region, read as the region's width in em. The arrow
keys step it 1em and Shift 4em, Home and End go to the narrowest and the widest, and Enter folds the navigation to the
icon rail and shuts the side panel. A drag follows the pointer, mouse, finger or pen, and a fold changes the columns
at once: animating them would lay the whole app out again on every frame. The widths are the shell's own, or the
caller's (navigationWidth, asideWidth), or kept per browser under layoutStorageKey, read on the first render so
the shell opens at the kept widths.
Log
02:00 UTC. At anchor in Morgat, holding. Wind southwest 18 kn.
Code
import { AppShell } from "@bosunkit/components/app-shell";
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { BookOpen, Cloud, LayoutDashboard, Waves } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"On a desktop the navigation and the side panel resize from the splitters on their edges, by drag or by the " +
"arrow keys, and Enter folds them; the widths stay in this browser under layoutStorageKey.";
const GROUPS: readonly NavGroup[] = [
{
id: "passage",
label: "Passage",
items: [
{ href: "#dashboard", label: "Dashboard", icon: <LayoutDashboard /> },
{ href: "#weather", label: "Weather", icon: <Cloud /> },
{ href: "#tides", label: "Tides", icon: <Waves /> },
{ href: "#log", label: "Log", icon: <BookOpen /> },
],
},
];
export default function Resizing(): ReactElement {
return (
<div style={{ width: "100%", overflowX: "auto", border: "1px solid var(--border)" }}>
<div style={{ width: "80em" }}>
<AppShell
stickyHeader={false}
layoutStorageKey="bosun-design-app-shell-resizing"
header={<p style={{ margin: 0, padding: "var(--space-sm) var(--space-lg)" }}>Log</p>}
navigation={<NavList groups={GROUPS} currentHref="#log" />}
main={<p style={{ margin: 0 }}>02:00 UTC. At anchor in Morgat, holding. Wind southwest 18 kn.</p>}
aside={<p style={{ margin: 0 }}>Next entry due at 03:00 UTC.</p>}
asideLabel="Log schedule"
navigationMinWidth={10}
asideMaxWidth={30}
/>
</div>
</div>
);
}
A console
mode="console" frames a display that stays on one view, such as conning or the night watch display. The header
keeps the alert zone on the alert layer, and the shell draws no navigation, menu button or tab bar, even when they are
given. splitPresets names the ways the screen divides between the page and the side panel ("Chart", "Rose and
chart"); where the panel stands beside the page, a segmented control at the header's end offers them, named "Layout".
Choosing one sets the panel's width within its bounds, as its splitter would, or shuts it for a width of 0, and the
split in use is the one checked. A width the crew drag to themselves checks none. A console usually fills the screen,
so it takes fillHeight.
Conning
Chart: Morgat anchorage, own vessel at anchor, swing circle 45 m.
Code
import { AppShell, type AppShellSplitPreset } from "@bosunkit/components/app-shell";
import type { ReactElement } from "react";
export const description: string =
"A conning console: the header keeps the alert zone, there is no navigation, and the named splits at the " +
"header's end divide the screen between the chart and the instruments.";
const SPLITS: readonly AppShellSplitPreset[] = [
{ id: "chart", label: "Chart", asideWidth: 0 },
{ id: "rose", label: "Rose and chart", asideWidth: 24 },
{ id: "instruments", label: "Instruments", asideWidth: 40 },
];
export default function Console(): ReactElement {
return (
<div style={{ width: "100%", overflowX: "auto", border: "1px solid var(--border)" }}>
<div style={{ width: "80em", height: "24em" }}>
<AppShell
mode="console"
fillHeight
splitPresets={SPLITS}
defaultAsideWidth={24}
asideMaxWidth={40}
header={<p style={{ margin: 0, padding: "var(--space-sm) var(--space-lg)" }}>Conning</p>}
main={<p style={{ margin: 0 }}>Chart: Morgat anchorage, own vessel at anchor, swing circle 45 m.</p>}
aside={<p style={{ margin: 0 }}>Heading 223°T, wind southwest 18 kn, depth 8.4 m.</p>}
asideLabel="Instruments"
/>
</div>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | AppShellMode | undefined | "app" | What the shell frames: an application screen, or a console with no navigation; app by default. |
| splitPresets | readonly AppShellSplitPreset[] | undefined | – | Named splits between the page and the side panel, offered in the header where the panel stands beside the page: on a desktop, a shell wider than 68.75em. Choosing one sets the panel's width as a drag on its splitter would. |
| splitPresetsLabel | string | undefined | – | The split buttons' group name; "Layout" in the screen's language when unset. |
| header | ReactNode | undefined | – | The bar at the top (HeaderBar), in the page's banner landmark. |
| navigation | ReactNode | undefined | – | The navigation (NavList), in a navigation landmark: a panel on a desktop, a rail on a tablet. |
| navigationFooter | ReactNode | undefined | – | Identity or other persistent controls below the navigation's independently scrolling destinations. |
| navigationLabel | string | undefined | – | The navigation landmark's name; "All pages" in the screen's language when unset, so it differs from the tab bar's "Main navigation" while the phone's menu shows both. |
| navigationMode | AppShellNavigationMode | undefined | "auto" | How the navigation column shows from tablet width up; auto by default. |
| mainrequired | ReactNode | – | The page, in the main landmark, which the skip link moves to. |
| aside | ReactNode | undefined | – | The side panel, in a complementary landmark: beside the page on a wide screen, under it on a narrow one. |
| asideLabel | string | undefined | – | The side panel's name; "Side panel" in the screen's language when unset. |
| footer | ReactNode | undefined | – | The foot of the screen, such as a telemetry strip, in the contentinfo landmark. |
| navigationBar | ReactNode | undefined | – | The phone's bottom navigation (NavigationBar), shown at phone width in place of the navigation column. Without one,
a phone gets a menu button at the start of the header that opens the navigation over the page. |
| menuOpen | boolean | undefined | – | Whether the navigation menu is open, for a caller that holds it (a tab bar's More opens it). |
| defaultMenuOpen | boolean | undefined | false | Whether the menu starts open, when the shell holds it itself. |
| onMenuOpenChange | ((open: boolean) => void) | undefined | – | Called with the menu's new state when the crew open or close it, or follow a link in it. |
| stickyHeader | boolean | undefined | true | Whether the header stays at the top of the window while the page scrolls, on the alert layer, so a dialog or the dimmer never covers the alert zone; on by default. The navigation then stays beside the page under it. |
| fillHeight | boolean | undefined | false | Whether the shell fills the height of its container, which must have a fixed height (the window's, or a device
screen's), rather than growing with its page. The page then scrolls inside the shell under the header, and the
shell is the bk-screen on both axes, so a component's @container bk-screen (max-height: …) query measures
it. Off by default: the shell grows with its page, and a height query inside it measures the nearest fixed-height
bk-screen around it. |
| resizable | boolean | undefined | true | Whether splitters on the navigation's and the side panel's edges resize them, where they stand beside the page: on a desktop, a shell wider than 68.75em. On by default; a phone or a tablet never shows them. |
| collapsible | boolean | undefined | true | Whether the navigation button and Enter on a splitter fold their region: navigation to a rail, aside shut. On by default. |
| navigationWidth | number | undefined | – | The navigation panel's width in em, for a caller that holds it. |
| defaultNavigationWidth | number | undefined | 14 | The navigation panel's width in em at first, when the shell holds it; 14. |
| onNavigationWidthChange | ((width: number) => void) | undefined | – | Called with the navigation's new width in em as the crew drag or step its splitter. |
| navigationMinWidth | number | undefined | 11 | The narrowest the navigation panel can be dragged, in em; 11. |
| navigationMaxWidth | number | undefined | 24 | The widest, in em; 24. |
| navigationCollapsed | boolean | undefined | – | Whether the navigation is folded to the icon rail, for a caller that holds it. |
| defaultNavigationCollapsed | boolean | undefined | false | Whether it starts folded, when the shell holds it. |
| onNavigationCollapsedChange | ((collapsed: boolean) => void) | undefined | – | Called when the crew fold or unfold the navigation from its button or splitter. |
| asideWidth | number | undefined | – | The side panel's width in em, for a caller that holds it. |
| defaultAsideWidth | number | undefined | 20 | The side panel's width in em at first, when the shell holds it; 20. |
| onAsideWidthChange | ((width: number) => void) | undefined | – | Called with the side panel's new width in em as the crew drag or step its splitter. |
| asideMinWidth | number | undefined | 14 | The narrowest the side panel can be dragged, in em; 14. |
| asideMaxWidth | number | undefined | 36 | The widest, in em; 36. |
| asideCollapsed | boolean | undefined | – | Whether the side panel is shut, for a caller that holds it. |
| defaultAsideCollapsed | boolean | undefined | false | Whether it starts shut, when the shell holds it. |
| onAsideCollapsedChange | ((collapsed: boolean) => void) | undefined | – | Called when the crew shut or open the side panel from its splitter. |
| layoutStorageKey | string | undefined | – | A storage key under which the shell keeps the widths and folds the crew set, per browser, through the kit's persisted store. The shell reads it on its first render in a browser, so it lays out at the kept sizes at once. |
| className | string | undefined | – | Classes added to the root's own. |
| testId | string | undefined | – | Becomes data-testid on the root; the menu button is ${testId}--menu. |
| ref | Ref<HTMLDivElement> | undefined | – | The root element. |
Header bar with the alert zone
The page.
Alert zone, then the page.
Persistent navigation identity
navigationFooter accepts a NavUser or other persistent controls. It stays below the independently scrolling
links and follows the column between panel and rail. Its account menu inherits the presentation scope and returns
focus to the trigger. The navigation's available height is measured below the sticky header.
See the account and submenu composition for a live workspace with a primary column, team submenu, local tabs and a user card. The caller supplies account actions; the shell owns only layout and navigation visibility.
Related
- Header bar: the bar in the header slot, with the alert zone and the status strip.
- Navigation list: the navigation slot's panel and rail.
- Navigation bar: the phone's navigation at the foot.
- Splitter: the handle on a region's edge.