Navigation list
Grouped links to the application's pages, with icons, counts and the current page marked, as a panel or an icon rail.
On this page
Code
import { NavList, NavListToggle, NavUser, type NavGroup, type NavItem } from "@bosunkit/components/nav-list";
import { Splitter } from "@bosunkit/components/splitter";
import {
Activity,
Anchor,
BookOpen,
Cloud,
Gauge,
LayoutDashboard,
ListChecks,
Map as MapIcon,
MessageSquare,
Settings,
Ship,
TriangleAlert,
Users,
Waves,
Wrench,
} from "lucide-react";
import { useId, useState, type Dispatch, type MouseEvent, type ReactElement, type SetStateAction } from "react";
export const description: string =
"Use the panel-to-rail button or the splitter to give space back to the page. Expanding restores the panel width, " +
"current page and closed groups. Scroll the list to reach every destination.";
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: "#conning", label: "Conning", icon: <Ship /> },
{ href: "#chart", label: "Chart", icon: <MapIcon />, count: { value: 7, label: "7 targets tracked" } },
{ href: "#instruments", label: "Instruments", icon: <Gauge /> },
{ 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 /> },
{ href: "#checklists", label: "Checklists", icon: <ListChecks /> },
],
},
{
id: "aboard",
label: "Aboard",
items: [
{ href: "#chat", label: "Chat", icon: <MessageSquare /> },
{ href: "#crew", label: "Crew", icon: <Users />, count: { value: 4, label: "4 people aboard" } },
],
},
{
id: "vessel",
label: "Vessel",
items: [
{
href: "#maintenance",
label: "Maintenance",
icon: <Wrench />,
count: { value: 2, label: "2 tasks due", priority: "caution" },
},
{ href: "#settings", label: "Settings", icon: <Settings /> },
{ href: "#telemetry", label: "Telemetry", icon: <Activity /> },
],
},
];
// The example owns its columns; NavListToggle and Splitter share the same fold state.
const LAYOUT: string = `
.nav-list-showcase { container: navigation-example / inline-size; width: 100%; }
.nav-list-showcase__frame {
display: flex;
height: calc(var(--target-touch-size) * 9);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.nav-list-showcase__column {
width: var(--nav-list-showcase-width);
max-width: 100%;
flex: none;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--surface);
}
.nav-list-showcase__toolbar {
display: flex;
justify-content: flex-end;
padding: var(--space-sm);
border-bottom: 1px solid var(--border);
}
.nav-list-showcase__scroll {
overflow: auto;
flex: 1 1 auto;
min-height: 0;
padding: var(--space-sm);
}
.nav-list-showcase__page {
flex: 1 1 auto;
min-width: 0;
padding: var(--space-lg);
}
@container navigation-example (max-width: 28em) {
.nav-list-showcase__frame[data-collapsed="false"] .nav-list-showcase__column { width: 100%; }
.nav-list-showcase__frame[data-collapsed="false"] .nav-list-showcase__page { display: none; }
.nav-list-showcase__splitter { display: none; }
}
`;
export default function Showcase(): ReactElement {
const columnId: string = useId();
const [width, setWidth]: [number, Dispatch<SetStateAction<number>>] = useState<number>(16);
const [folded, setFolded]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
const [current, setCurrent]: [string, Dispatch<SetStateAction<string>>] = useState<string>("#situation");
const [closed, setClosed]: [readonly string[], Dispatch<SetStateAction<readonly string[]>>] = useState<
readonly string[]
>(["vessel"]);
const currentLabel: string =
GROUPS.flatMap((group: NavGroup): readonly NavItem[] => group.items).find(
(item: NavItem): boolean => item.href === current,
)?.label ?? "Dana Whitlock’s profile";
function follow(event: MouseEvent<HTMLElement>): void {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
)
return;
const link: HTMLAnchorElement | null = event.target instanceof Element ? event.target.closest("a") : null;
const href: string | null | undefined = link?.getAttribute("href");
if (href?.startsWith("#") === true) {
event.preventDefault();
setCurrent(href);
}
}
return (
<div className="nav-list-showcase">
<style href="bosunkit-nav-list-showcase" precedence="default">
{LAYOUT}
</style>
<div className="nav-list-showcase__frame" data-collapsed={folded ? "true" : "false"}>
<nav
id={columnId}
aria-label="Example pages"
onClick={follow}
className="nav-list-showcase__column"
style={{ "--nav-list-showcase-width": `${String(folded ? 4.5 : width)}em` }}
>
<div className="nav-list-showcase__toolbar">
<NavListToggle collapsed={folded} controls={columnId} onCollapsedChange={setFolded} />
</div>
<div className="nav-list-showcase__scroll">
<NavList
groups={GROUPS}
currentHref={current}
mode={folded ? "rail" : "panel"}
closedGroups={closed}
onClosedGroupsChange={setClosed}
/>
</div>
<div style={{ flex: "none", padding: "var(--space-sm)", borderTop: "1px solid var(--border)" }}>
<NavUser
name="Dana Whitlock"
description="Crew coordinator"
menuLabel="Account actions for Dana Whitlock"
mode={folded ? "rail" : "panel"}
actions={[
{ id: "profile", label: "View profile" },
{ id: "preferences", label: "Account preferences" },
]}
onSelect={(action: string): void => {
setCurrent(action === "profile" ? "#profile" : "#settings");
}}
/>
</div>
</nav>
<Splitter
className="nav-list-showcase__splitter"
label="Resize the navigation"
controls={columnId}
panel="start"
value={width}
min={11}
max={22}
step={1}
largeStep={4}
pixelsPerUnit={16}
collapsible
collapsed={folded}
collapsedValue={4.5}
onCollapsedChange={setFolded}
onValueChange={setWidth}
/>
<section className="nav-list-showcase__page" aria-label="Example page">
<h3 style={{ margin: 0, font: "var(--bk-type-heading-component)" }}>{currentLabel}</h3>
<p style={{ font: "var(--bk-type-body)", color: "var(--text-dim)" }}>
Choose a page in the navigation. Its selection stays when you switch between panel and rail.
</p>
</section>
</div>
</div>
);
}
The navigation list is the app shell's navigation: the application's pages in groups, each a link with its icon and
name, and the page on screen marked with aria-current="page", a raised surface and the accent on its icon. It reads
no data and holds no route. The links go through the provider's link component, so ui/web's router handles a path
inside the application, and a web address goes to a plain anchor; the caller says which page is current with
currentHref.
A destination can carry a count. An alert count takes its priority's color and its IEC 62923 shape before the number, so the color never speaks alone; any other count, such as the targets tracked, is drawn in the accent. Above 99 a count reads 99+, and each is read aloud by its words, so a screen reader hears "Situation, 1 unacknowledged alert".
Each group's heading opens and closes it. A closed group keeps its alert counts on its heading, so an alert waiting
on a page the crew folded away stays in sight. The caller can hold which groups are closed (closedGroups), as ui/web
keeps them per browser.
The list measures its own column. Narrower than 9em it is a rail: icons in a column, each link named in a tooltip on
hover and focus, the counts on the icons' corners, every group shown and ruled off from the next. mode holds the
panel or the rail at any width. Every link and heading is at least a touch target tall.
The list sets no width of its own; its column does. NavListToggle requests a change between panel and rail. The
column owner updates the width and mode together, keeping the last panel width to restore on expansion. The button
stays reachable, has a translated name and tooltip, and exposes aria-expanded and aria-controls. Closing groups
and selecting a destination are independent of folding the column.
In the app shell, the navigation column is a rail on a tablet and a panel on a desktop. The desktop panel has this toggle and an optional splitter, both using the same fold state. A phone uses the shell's menu button. The hero demonstrates a standalone column: at narrow widths it gives the panel the available width; folding it reveals the page beside its rail. The list scrolls below its toggle.
Examples
A closed group
Code
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { Anchor, BookOpen, TriangleAlert, Waves } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"A closed group keeps its alert counts on its heading, with the priority's shape, so a closed Underway still " +
"shows the warning waiting on Situation.";
const GROUPS: readonly NavGroup[] = [
{
id: "underway",
label: "Underway",
items: [
{
href: "#situation",
label: "Situation",
icon: <TriangleAlert />,
count: { value: 2, label: "2 unacknowledged alerts", priority: "warning" },
},
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
],
},
{
id: "passage",
label: "Passage",
items: [
{ href: "#tides", label: "Tides", icon: <Waves /> },
{ href: "#log", label: "Log", icon: <BookOpen /> },
],
},
];
export default function ClosedGroup(): ReactElement {
return (
<div style={{ width: "16em", maxWidth: "100%" }}>
<NavList groups={GROUPS} currentHref="#tides" defaultClosedGroups={["underway"]} />
</div>
);
}
As a rail
Code
import { NavList, type NavGroup } from "@bosunkit/components/nav-list";
import { Anchor, Cloud, LayoutDashboard, Map as MapIcon, MessageSquare, TriangleAlert } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"As a rail: icons in a column, each link named in a tooltip on hover and focus, a count on its icon's corner, and " +
"the groups ruled off from each other.";
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 /> },
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
],
},
{
id: "aboard",
label: "Aboard",
items: [
{ href: "#weather", label: "Weather", icon: <Cloud /> },
{ href: "#chat", label: "Chat", icon: <MessageSquare /> },
],
},
];
export default function Rail(): ReactElement {
return (
<div style={{ width: "4.5em" }}>
<NavList groups={GROUPS} currentHref="#chart" mode="rail" />
</div>
);
}
By the column's width
Code
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 =
"The same list in mode auto in a 4.5em column and a 14em one: under 9em it draws the rail, wider it draws labels.";
const GROUPS: readonly NavGroup[] = [
{
id: "pages",
label: "Underway",
items: [
{ href: "#dashboard", label: "Dashboard", icon: <LayoutDashboard /> },
{ href: "#anchor", label: "Anchor", icon: <Anchor /> },
{ href: "#weather", label: "Weather", icon: <Cloud /> },
],
},
];
export default function ByColumnWidth(): ReactElement {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-xl)", alignItems: "flex-start" }}>
<div style={{ width: "4.5em" }}>
<NavList groups={GROUPS} currentHref="#anchor" />
</div>
<div style={{ width: "14em", maxWidth: "100%" }}>
<NavList groups={GROUPS} currentHref="#anchor" />
</div>
</div>
);
}
Without headings
Code
import { NavList } from "@bosunkit/components/nav-list";
import { Activity, Settings } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"A group with no label has no heading and does not close: the settings and telemetry at the foot of a rail or " +
"a panel.";
export default function WithoutHeadings(): ReactElement {
return (
<div style={{ width: "16em", maxWidth: "100%" }}>
<NavList
groups={[
{
id: "foot",
items: [
{ href: "#telemetry", label: "Telemetry", icon: <Activity /> },
{ href: "#settings", label: "Settings", icon: <Settings /> },
],
},
]}
/>
</div>
);
}
Account and submenu composition
Code
import { DevicePreview, openingDevice } from "../../../../app/components/hero-preview";
import { AppShell } from "@bosunkit/components/app-shell";
import { Breadcrumbs } from "@bosunkit/components/breadcrumbs";
import { Button } from "@bosunkit/components/button";
import { NavList, NavUser, type NavGroup } from "@bosunkit/components/nav-list";
import { SearchInput } from "@bosunkit/components/search-input";
import { Tabs } from "@bosunkit/components/tabs";
import { BookOpen, Compass, House, Settings, Users, Wrench } from "lucide-react";
import {
useState,
type ChangeEvent,
type Dispatch,
type MouseEvent,
type ReactElement,
type SetStateAction,
} from "react";
export const description: string =
"A crew workspace separates application destinations, team selection and local tabs. Filter teams, change the selection, open Dana's account menu and compare the available width with a wide desktop. All records are illustrative.";
const groups: readonly NavGroup[] = [
{
id: "work",
label: "Workspace",
items: [
{ href: "#workspace-home", label: "Overview", icon: <House /> },
{ href: "#workspace-crew", label: "Crew", icon: <Users /> },
{ href: "#workspace-reports", label: "Reports", icon: <BookOpen /> },
],
},
{
id: "vessel",
label: "Vessel",
items: [
{ href: "#workspace-equipment", label: "Equipment", icon: <Wrench /> },
{ href: "#workspace-preferences", label: "Preferences", icon: <Settings /> },
],
},
];
interface Team {
id: string;
label: string;
detail: string;
people: readonly string[];
}
const teams: readonly [Team, ...Team[]] = [
{
id: "deck",
label: "Deck team",
detail: "Navigation and deck operations",
people: ["Nadia Okafor", "Tom Rivers", "Aline Sousa"],
},
{ id: "engineering", label: "Engineering", detail: "Machinery and systems", people: ["Marek Kowal", "Priya Menon"] },
{
id: "hospitality",
label: "Hospitality",
detail: "Guest services and provisioning",
people: ["Esme Laurent", "Kofi Mensah"],
},
];
const layout: string = `
.nav-workspace__viewport { max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); }
.nav-workspace__screen { height: calc(var(--target-touch-size) * 12); }
.nav-workspace__viewport:has([data-wide="true"]) { width: 100%; height: 100%; }
.nav-workspace__screen[data-wide="true"] { height: 100%; }
.nav-workspace__panes { display: grid; grid-template-columns: minmax(0, var(--bk-navigation-submenu-width)) minmax(0, 1fr); min-height: calc(var(--target-touch-size) * 9); }
.nav-workspace__teams { padding-inline-end: var(--space-lg); border-inline-end: 1px solid var(--border); display: grid; align-content: start; gap: var(--space-lg); }
.nav-workspace__content { padding-inline-start: var(--bk-navigation-content-inset); min-width: 0; }
.nav-workspace__people { list-style: none; margin: 0; padding: 0; }
.nav-workspace__people li { padding-block: var(--space-lg); border-bottom: 1px solid var(--border); }
@container bk-screen (max-width: 55em) { .nav-workspace__panes { grid-template-columns: minmax(0, 1fr); gap: var(--space-xl); } .nav-workspace__teams { padding-inline-end: 0; border-inline-end: 0; } .nav-workspace__content { padding-inline-start: 0; } }
`;
export default function Workspace(): ReactElement {
const [wide, setWide]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
const [destination, setDestination]: [string, Dispatch<SetStateAction<string>>] = useState("#workspace-crew");
const [teamId, setTeamId]: [string, Dispatch<SetStateAction<string>>] = useState("deck");
const [query, setQuery]: [string, Dispatch<SetStateAction<string>>] = useState("");
const [tab, setTab]: [string, Dispatch<SetStateAction<string>>] = useState("people");
const [account, setAccount]: [string, Dispatch<SetStateAction<string>>] = useState("");
const team: Team = teams.find((item: Team): boolean => item.id === teamId) ?? teams[0];
const shown: readonly Team[] = teams.filter((item: Team): boolean =>
`${item.label} ${item.detail}`.toLowerCase().includes(query.toLowerCase()),
);
const title: string =
groups
.flatMap((group: NavGroup): readonly { href: string; label: string }[] => group.items)
.find((item: { href: string; label: string }): boolean => item.href === destination)?.label ?? "Crew";
function follow(event: MouseEvent<HTMLDivElement>): void {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
event.altKey
)
return;
const link: HTMLAnchorElement | null = event.target instanceof Element ? event.target.closest("a") : null;
const href: string = link?.getAttribute("href") ?? "";
if (href.startsWith("#workspace-")) {
event.preventDefault();
setDestination(href);
setAccount("");
}
if (href.startsWith("#team-")) {
event.preventDefault();
setTeamId(href.slice(6));
setTab("people");
}
}
const screen: ReactElement = (
<div className="nav-workspace__viewport" onClick={follow}>
<div className="nav-workspace__screen" data-wide={wide}>
<AppShell
fillHeight
defaultNavigationWidth={18}
resizable={false}
navigationLabel="Workspace destinations"
header={
<div style={{ padding: "var(--space-lg)", font: "var(--bk-type-label)" }}>Bosun · Crew workspace</div>
}
navigation={<NavList groups={groups} currentHref={destination} collapsible={false} />}
navigationFooter={
<NavUser
name="Dana Whitlock"
description="Crew coordinator"
menuLabel="Dana Whitlock account menu"
avatar={<span>DW</span>}
actions={[
{ id: "profile", label: "View profile", icon: <Users /> },
{ id: "preferences", label: "Account preferences", icon: <Settings /> },
]}
onSelect={(action: string): void => {
setAccount(
action === "profile" ? "Dana Whitlock · Crew coordinator" : "Account preferences · Local preview",
);
}}
/>
}
main={
account !== "" ? (
<section>
<h3>{account}</h3>
<Button
onClick={(): void => {
setAccount("");
}}
>
Back to workspace
</Button>
</section>
) : destination !== "#workspace-crew" ? (
<section>
<h3>{title}</h3>
<p>
This local example shows the {title.toLowerCase()} destination. Choose Crew to explore its team
submenu.
</p>
</section>
) : (
<div className="nav-workspace__panes">
<section className="nav-workspace__teams" aria-label="Team submenu">
<h3 style={{ margin: 0, font: "var(--bk-type-heading-component)" }}>All teams</h3>
<SearchInput
aria-label="Filter teams"
placeholder="Filter teams"
value={query}
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
setQuery(event.currentTarget.value);
}}
onClear={(): void => {
setQuery("");
}}
clearLabel="Clear team filter"
/>
<nav aria-label="Teams">
<NavList
mode="panel"
currentHref={`#team-${teamId}`}
groups={[
{
id: "teams",
items: shown.map(
(item: Team): { href: string; label: string; description: string; icon: ReactElement } => ({
href: `#team-${item.id}`,
label: item.label,
description: item.detail,
icon: <Compass />,
}),
),
},
]}
/>
</nav>
{shown.length === 0 && <p role="status">No matching teams. Clear the filter to see all teams.</p>}
</section>
<section className="nav-workspace__content" aria-label="Team content">
<Breadcrumbs
label="Team hierarchy"
expandLabel="Show ancestors"
collapseLabel="Hide ancestors"
items={[
{ id: "crew", label: "Crew", href: "#workspace-crew" },
{ id: team.id, label: team.label },
]}
/>
<h3>{team.label}</h3>
<p>{team.detail}</p>
<Tabs
tabs={[
{ id: "people", label: "People" },
{ id: "responsibilities", label: "Responsibilities" },
]}
value={tab}
onValueChange={setTab}
>
<Tabs.Panel value="people">
<ul className="nav-workspace__people">
{team.people.map((name: string): ReactElement => (
<li key={name}>{name}</li>
))}
</ul>
</Tabs.Panel>
<Tabs.Panel value="responsibilities">
<p>{team.detail}. Team assignments in this preview are illustrative.</p>
</Tabs.Panel>
</Tabs>
</section>
</div>
)
}
/>
</div>
</div>
);
return (
<div style={{ width: "100%", display: "grid", gap: "var(--space-md)" }}>
<style href="bosunkit-nav-workspace" precedence="default">
{layout}
</style>
<div>
<Button
variant="ghost"
onClick={(): void => {
setWide(!wide);
}}
>
Preview wide desktop
</Button>
</div>
{wide ? (
<DevicePreview
initial={openingDevice("desktop")}
background="var(--bg)"
controls={null}
onClose={(): void => {
setWide(false);
}}
>
{screen}
</DevicePreview>
) : (
screen
)}
</div>
);
}
NavUser shows the current user's avatar, name and a supporting role or organization. The whole card opens a
named account menu; there is one focus target, with no nested button inside another button. The caller supplies
translated actions and handles their effects. The rail keeps the avatar and accessible name; opening its menu
shows the full identity. A generic user mark is the fallback when no avatar is supplied.
Place it in the app shell's navigationFooter, outside the scrolling destinations. In a standalone column, use
one flexible scroll region for links and a separate, nonshrinking footer. Avoid fixed positioning against the
browser window: a navigation column can live inside an embedded application or a smaller device preview.
A destination's optional description supports a secondary navigation column, such as a team and its remit.
Keep primary destinations, secondary selection and local tabs as separate regions with distinct accessible names.
The wide workspace example shows all three; at narrow widths the team selector precedes its content.
Spacing and hierarchy
The list uses --bk-navigation-group-gap between groups, --bk-navigation-item-gap between destinations and
--bk-navigation-item-inset inside each row. Group spacing is deliberately larger than row spacing. Collapsible
headings retain their touch target; a static heading uses the compact Section heading role.
Column width belongs to the shell. The default app shell uses its compact navigation width; the wide workspace
uses --bk-navigation-column-width and --bk-navigation-submenu-width as its composition roles. The content
inset after the secondary divider uses --bk-navigation-content-inset. A divider marks the boundary; content
padding creates the breathing room on its other side. Avoid stacking several untracked margins at that edge.
| Token | Value | rem | Preview |
|---|---|---|---|
--bk-navigation-group-gap | var(--space-xl) | — | — |
--bk-navigation-item-gap | var(--space-xs) | — | — |
--bk-navigation-item-inset | var(--space-md) | — | — |
--bk-navigation-avatar-size | 2em | — | — |
--bk-navigation-menu-width | 16em | — | — |
--bk-navigation-column-width | 18em | — | — |
--bk-navigation-submenu-width | 18em | — | — |
--bk-navigation-content-inset | var(--space-xl) | — | — |
--bk-navigation-icon-size | 1.25em | — | — |
--bk-navigation-line-width | 1px | 0.0625rem |
Props
NavList
| Prop | Type | Default | Description |
|---|---|---|---|
| groupsrequired | readonly NavGroup[] | – | The destinations, by group. |
| currentHref | string | undefined | – | The href of the page on screen; its link carries aria-current="page". |
| mode | NavListMode | undefined | "auto" | auto (the default) draws an icon rail while the list is narrower than 9em and labels when it is wider, so the
column the app shell gives it decides; panel and rail hold one form at every width. In the rail the labels
are hidden, still read aloud, and shown in a tooltip; the group headings give way to a rule and every group
shows its destinations. |
| headingLevel | NestedHeadingLevel | undefined | 2 | The level of the group headings, so they sit in the page's outline; 2 by default. |
| collapsible | boolean | undefined | true | Whether a group's heading opens and closes the group; on by default. |
| closedGroups | readonly string[] | undefined | – | The ids of the closed groups, for a caller that holds them (ui/web stores them per browser). |
| defaultClosedGroups | readonly string[] | undefined | [] | The groups closed at first, when the list holds the state itself. |
| onClosedGroupsChange | ((closed: readonly string[]) => void) | undefined | – | Called with the closed groups' ids when the crew open or close one. |
| className | string | undefined | – | Classes added to the root's own. |
| testId | string | undefined | – | Becomes data-testid on the root; a group's heading button is ${testId}--toggle-<group id>. |
| ref | Ref<HTMLDivElement> | undefined | – | The root element. |
NavListToggle
The column owner passes collapsed, onCollapsedChange and the controlled region's id as controls. The button
uses the shared icon-button target, focus treatment and tooltip. It requests state changes; it does not resize a
region by itself.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | undefined | "ghost" | The button's emphasis: primary for the main action, danger for a destructive one, ghost for a quiet one. |
| textCase | ButtonTextCase | undefined | – | How the label is cased: in capitals by default. none keeps the letters as written, for a label whose case
carries meaning, such as a chess move in SAN (Nf3, exd5), a call sign or a chart number. |
| motion | MotionProp | undefined | – | How the button moves. By default it fades its new label in when the label changes ("Acknowledge" to
"Acknowledged"), keeping its focus, and has no entrance. true also scales it in as it mounts, wherever it is,
and out when it leaves a MotionPresence; a preset (fade, rise, scale) or { enter, exit, change } chooses;
false turns it off.
It is named and pressable from the first frame. |
| ref | Ref<HTMLButtonElement> | undefined | – | The button element. |
| shortcut | string | undefined | – | A keyboard shortcut shown after the label in a key chip, such as Ctrl K, when the control has one. |
| testId | string | undefined | – | Becomes data-testid on the tooltip's trigger, which is the button. |
| collapsedrequired | boolean | – | Whether the navigation column is folded to its icon rail. |
| controlsrequired | string | – | The id of the navigation region whose column changes width. |
| onCollapsedChangerequired | (collapsed: boolean) => void | – | Requests a fold or expansion; the column owner updates its width and the list's mode together. |
Also takes every attribute of Omit<
IconButtonProps,
"label" | "icon" | "onClick" | "aria-expanded" | "aria-controls"
>.
NavUser
| Prop | Type | Default | Description |
|---|---|---|---|
| namerequired | string | – | The current user's display name. |
| description | string | undefined | – | A role, organization or other short identity detail. |
| avatar | ReactNode | undefined | – | Decorative avatar, such as an image with empty alt text; a generic user icon is the fallback. |
| menuLabelrequired | string | – | Complete translated trigger and menu name, including the user's name. |
| actionsrequired | readonly NavUserAction[] | – | Available account actions. An empty list disables the trigger. |
| onSelectrequired | (id: string) => void | – | Requests an action; it never signs out, changes an account or navigates by itself. |
| mode | NavListMode | undefined | "auto" | Auto follows the available column width; panel and rail force a form. |
| open | boolean | undefined | – | Controlled menu visibility. |
| defaultOpen | boolean | undefined | – | Initial uncontrolled menu visibility. |
| onOpenChange | ((open: boolean) => void) | undefined | – | Reports menu opening and dismissal. |
| className | string | undefined | – | Additional wrapper classes. |
| testId | string | undefined | – | Wrapper test id; trigger and menu use --trigger and --menu suffixes. |
| ref | Ref<HTMLButtonElement> | undefined | – | The account trigger. |
Related
-
Action menu: the shared menu used by account controls.
-
App shell: the navigation slot, a rail on a tablet and a panel on a desktop.
-
Navigation bar: the phone's navigation at the foot of the screen.
-
Navigation: choosing hierarchy, page, collection and task navigation.
-
Command palette: direct lookup of destinations and actions.