Skip to content

Sidebar / nav rail

In review
Live · web

Sidebar

dizyx · nockerl-design

Streaming · 2 tools running

nockerl-design · docs site

Designing the sidebar to the token-reactive standard.

Expanded · viewing nockerl-design · docs site. Tab in, arrow-key the rows, expand Tasks, collapse to the rail. The sidebar stays in the layout. The island is live.

// Web: React, consuming @dizyx/nockerl-tokens. This is the spec plus the live demo above.
// It is not yet exported from @dizyx/nockerl-react.
export function Shell() {
const [section, setSection] = useState('chat');
const [collapsed, setCollapsed] = useState(false);
return (
<div className="layout">
<Sidebar
collapsed={collapsed} // expanded ↔ icon-only rail
onToggleCollapse={() => setCollapsed((c) => !c)}
selected={section}
onSelect={setSection}
sections={[
{ label: 'Workspace', items: [
{ id: 'home', label: 'Home', icon: <HomeIcon /> },
{ id: 'chat', label: 'Chat', icon: <ChatIcon />, dot: 'streaming' },
{ id: 'tasks', label: 'Tasks', icon: <TasksIcon />, badge: { count: 3, tone: 'attention' },
children: [{ id: 'tasks-mine', label: 'Assigned to me' }, { id: 'tasks-review', label: 'In review' }] },
{ id: 'files', label: 'Files', icon: <FilesIcon />, badge: { count: 12 } },
] },
]}
footer={<ProfileMenu name="Ada Lovelace" onSettings={openSettings} />}
/>
<main>{/* the content region; the sidebar stays beside it, never over it */}</main>
</div>
);
}
PropTypeDefaultDescription
sections *NavSection[]Grouped nav: { label, items }. Each item is { id, label, icon, badge?, dot?, children? } and renders as one focusable button. Grouping is framework-original and ships on no platform yet: the Swift sidebar is a flat list.
selected *stringActive destination id (a leaf, meaning a child id when a sub-item is active). Selection reads by outline (law §6): accent border + accent ink + the icon, with no wash and never a stripe.
onSelect *(id: string) => voidFired when a (leaf) item is activated: click / Enter / Space.
focus + keyboard *behaviorEvery row stays in the focus chain and shows a visible focus ring, drawn with outline on :focus-visible (never a colored shadow, law §1). The shell owns roving tabindex, not the row: one row is tabbable at a time, arrow keys move between rows, Home / End jump to the ends, and Enter / Space activate. Full keyboard operability is law §14, so the macOS focus stance is prohibited here.
collapsedbooleanfalseCollapses the sidebar to the icon-only rail; labels + counts hide and each item exposes its label via tooltip / aria-label. Framework-original, not yet on Swift, whose panel is a fixed width with no rail.
onToggleCollapse() => voidToggles collapsed from the rail button. Framework-original, not yet on Swift.
badge{ count: number; tone?: 'neutral' | 'attention' }Per-item trailing count. neutral = mono chip; attention = warm count (status signal). Right-aligned; collapses to a corner dot on the rail. Framework-original, not yet on Swift.
childrenNavSubItem[]Nested sub-items on an item, which turn its row into an expandable disclosure (aria-expanded), indented under the parent. Framework-original, not yet on Swift: NockerlNavRow is deliberately a strict subset with no children, chevron, or expansion state.
footerReactNodePinned to the bottom. The settings entry is a peer destination held out of the main list, rendered by the same row component with the same selected treatment, so no separate footer vocabulary is needed. The profile (avatar + name) is framework-original and ships nowhere today.
Cross-platform drift

The sidebar is the persistent, in-layout nav column, the inline role of the ONE nav-surface: NockerlNavSurface (= NockerlDrawer) is a single edge-anchored surface with mode: inline | overlay. inline is this persistent rail; overlay is the drawer (a scrim panel that opens / closes). App shell composes ONE nav-surface, not two. The sidebar never floats over content and never dims it. Its rail chrome (grouped sections, collapse-to-rail, footer) is the reference inline nav content, and the shell-level convergence lands with App shell.

Platforms are allowed to diverge while the migration runs. What follows is the honest state today, not a defect list.

  • Selection reads by outline on every platform. Accent border, accent ink, and the leading icon, with no wash, no left rail, and no stripe (law §6). Both the Swift row and the web row now carry it; the soft cyan wash the web once used is gone.
  • Hover on an already-selected row differs on purpose. Swift answers nothing: on macOS a selected sidebar row is inert under the pointer, Voice ships that, and it is approved. The web brightens the row’s existing cyan edge to full strength instead, because a pointer-first surface that answers nothing on hover reads as disabled. It reuses the channel selection already owns rather than adding a wash, so law §6 still holds, and a border colour is interpolatable, so law §7 does too. This is the platform behaviour honoured per law §9, not drift: the learning transfers, the macOS habit does not.
  • Android has no side rail. Voice and web use a vertical sidebar; Android’s persistent nav is top chrome (project + session rows that collapse up under the title bar). A NavigationRail / PermanentNavigationDrawer is not used today.
  • Collapse: web collapses to an icon-only rail (with tooltips). The Swift panel is a fixed containerXs width with no rail, and Android collapses the chrome upward, not sideways. Collapse is framework-original and has not shipped on Swift.
  • Grouped sections, sub-items, counts, and the profile footer are framework-original. None has shipped on Swift, whose sidebar is a flat list of five destinations. They are kept here as the reference web pattern, not reported as harvested from a shipping app.
  • Scope differs. The Swift sidebar belongs to one window, not to the app: Voice is menu-bar-first, and a native status-item menu remains after the window closes. That menu is a macOS affordance the framework does not model, and the web has no equivalent.
  • Keyboard support does not transfer. The Swift rows leave the focus chain and have no arrow-key traversal, which is defensible on macOS and prohibited on the web. See the Swift focus row above.