Navigation
Choose patterns for application destinations, page hierarchy, peer views, collections and task progress.
On this page
Engineering reviews, daily updates and feature reports.
Illustrative entry from page 1
Illustrative entry from page 1
Illustrative entry from page 1
All reports, page 1.
Code
import { Breadcrumbs } from "@bosunkit/components/breadcrumbs";
import { Button } from "@bosunkit/components/button";
import { CommandPalette, type CommandGroup, type CommandItem } from "@bosunkit/components/command-palette";
import { Pagination, type PaginationLabels } from "@bosunkit/components/pagination";
import { Tabs } from "@bosunkit/components/tabs";
import { Search } from "lucide-react";
import { useId, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A report-library composition: breadcrumbs identify location, tabs change the collection, pagination changes the visible page, and the command menu offers a direct route. All results are illustrative.";
const labels: PaginationLabels = {
previous: "Previous library page",
next: "Next library page",
first: "First library page",
last: "Last library page",
page: (page: number): string => `Library page ${String(page)}`,
summary: (page: number, total: number): string => `Page ${String(page)} of ${String(total)}`,
};
const groups: readonly CommandGroup[] = [
{
id: "library",
label: "Report library",
items: [
{ id: "all", label: "Show all reports" },
{ id: "saved", label: "Show saved reports" },
],
},
];
export default function Showcase(): ReactElement {
const [tab, setTab]: [string, Dispatch<SetStateAction<string>>] = useState("all");
const [page, setPage]: [number, Dispatch<SetStateAction<number>>] = useState(1);
const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
const resultId: string = useId();
function choose(value: string): void {
setTab(value);
setPage(1);
}
return (
<div style={{ display: "grid", gap: "var(--space-lg)", width: "100%" }}>
<Breadcrumbs
label="Library hierarchy"
expandLabel="Show library ancestors"
collapseLabel="Hide library ancestors"
items={[
{ id: "design", label: "Design", href: "/" },
{ id: "components", label: "Components", href: "/docs/components" },
{ id: "library", label: "Report library example" },
]}
/>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "center",
gap: "var(--space-md)",
}}
>
<strong>Report library</strong>
<Button
variant="ghost"
onClick={(): void => {
setOpen(true);
}}
>
<Search size="1em" aria-hidden="true" />
Find a collection
</Button>
</div>
<Tabs
tabs={[
{ id: "all", label: "All reports" },
{ id: "saved", label: "Saved reports" },
]}
value={tab}
onValueChange={choose}
>
<Tabs.Panel value="all">
<p>Engineering reviews, daily updates and feature reports.</p>
</Tabs.Panel>
<Tabs.Panel value="saved">
<p>Your selected reading list in this local example.</p>
</Tabs.Panel>
</Tabs>
<div id={resultId} style={{ display: "grid", gap: "var(--space-sm)" }}>
{["Engineering review", "Daily update", "Feature report"].map((label: string): ReactElement => (
<div
key={label}
style={{ padding: "var(--space-md)", background: "var(--surface2)", borderRadius: "var(--radius-md)" }}
>
<strong>{label}</strong>
<p style={{ margin: 0, color: "var(--text-dim)" }}>
Illustrative {tab === "saved" ? "saved " : ""}entry from page {String(page)}
</p>
</div>
))}
</div>
<Pagination
label="Library results"
pageCount={tab === "all" ? 12 : 3}
value={page}
onValueChange={setPage}
labels={labels}
controls={resultId}
/>
<p role="status">
{tab === "all" ? "All reports" : "Saved reports"}, page {String(page)}.
</p>
<CommandPalette
open={open}
onOpenChange={setOpen}
title="Find a collection"
closeLabel="Close collection finder"
searchLabel="Search collections"
clearLabel="Clear collection search"
emptyText="No matching collections."
loadingText="Loading collections…"
groups={groups}
onSelect={(item: CommandItem): void => {
choose(item.id);
}}
/>
</div>
);
}
Navigation helps a reader locate the current page, choose another destination and understand what a control will change. Combine patterns by purpose and keep their state tied to the route or task that owns it.
Choose a pattern
| Need | Component | State owner |
|---|---|---|
| Persistent destinations with groups and a panel/rail form | Navigation list | Router supplies the current href; shell owns column size |
| A small set of primary destinations on a phone | Navigation bar | Router supplies the current destination; shell owns More |
| Ancestors of the current page | Breadcrumbs | Router or content hierarchy supplies the path |
| Peer views within one subject | Tabs | Caller owns active view and its panels |
| Pages of a bounded collection | Pagination | URL or collection controller owns page, filters and requests |
| Named stages in a task | Steps | Workflow owns validation, completion and draft answers |
| Direct lookup of a destination or action | Command palette | Shell owns opening; router/action layer owns the selected command |
A single screen may use several of these. In the specimen, breadcrumbs show hierarchy, tabs choose a collection, pagination changes the visible slice, and a command menu can select that collection directly. Side navigation and a phone tab bar belong in the App shell, surrounding the page content.
Primary, secondary and content regions
Use one persistent column for application destinations. A second column can select an object within the current area: a team, folder or project. Breadcrumbs then locate that object, and tabs switch views of the same object. The workspace example combines these regions with a bottom account menu and a wide desktop preview.
Give rows a consistent target and a small inter-row gap. Use a larger gap between labeled destination groups. Keep a shared inset on each side of a column divider, with more space before reading content than between list items. The navigation tokens name group, row, column and content spacing separately. A wider column should reveal more of a label, without enlarging the type or stretching row height.
Account identity belongs at the foot of the primary column. NavUser exposes a named menu with caller-owned
actions, while the app shell's navigationFooter keeps that control outside the scrolling links.
URLs and browser history
Use real href values for destinations, ancestor links and route-based pagination. Keep query, sort, filter and
page in the URL when readers need to share or revisit the same view. Respond to browser Back and Forward by reading
that state again, not by maintaining a second, disconnected page index.
The kit's provider accepts a link adapter for internal routes. Forward native anchor props, refs and event handlers to preserve accessible names, modified clicks and browser behavior. A command-menu option is an action selection; main-navigation links remain the route for copying a URL or opening a destination in another tab.
Current state and focus
| Pattern | Meaningful state | Keyboard behavior |
|---|---|---|
| Destination or page link | aria-current="page" | Tab and Enter; browser link modifiers |
| Tab | aria-selected, associated tabpanel | Arrow/Home/End movement; automatic or manual activation |
| Workflow stage | aria-current="step", explicit status and disabled state | Tab, Enter and Space |
| Command result | Active descendant in a named listbox | Input editing, arrows and explicit Enter selection |
Selection and keyboard focus remain distinct. A route transition should announce or focus its new heading when appropriate. A tab should follow its activation model. Paging should preserve the controls during loading and announce accepted results. A command dialog restores focus on dismissal. None of these components determines whether a server operation succeeded.
Responsive composition
Use the shell's navigation column on a wide screen and its small primary destination set on a phone. Keep the current destination and essential names available when switching between a panel and rail. Breadcrumbs disclose middle ancestors, pagination offers a compact summary, and steps can become vertical. Large type and translated labels must fit without shrinking targets or relying on a tooltip for essential content.
Navigation uses interface body, label and caption roles. Current states use the shared selected surface and action foreground; focus uses its independent ring. Report-reading typography stays in the document body. Directional icons and decorative artwork accompany names rather than replacing them.
| 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 |
Loading, unavailable destinations and errors
Keep an unavailable destination visible when its presence explains the task, with a reason and the appropriate disabled behavior. Do not turn an unavailable command into a link that appears to work. Keep request failures near the affected collection, and provide retry without erasing a query or draft.
Progress is explicit: reaching a later workflow screen does not mark earlier work complete. A missing total is not an empty collection. A highlighted command is not an executed action. The caller owns each of these decisions.
Hierarchy above the heading; local views above content; page controls beside collection results.
One tab row mixes pages, filters, workflow stages and destructive actions.
Related
- App shell: navigation regions and responsive layout.
- Accessibility: landmarks, focus and target sizes.
- Interaction states: selected, disabled and focused roles.
- Content: names that explain where a control leads.