SideNav
A docs sidebar of folding sections, with a collapse bar: docked on wide screens, opened over the page on phones.
SourcePackage @kerf-js/veneer
Preview
tsx
import { state } from '@kerf-js/core'import { SideNav } from '@kerf-js/veneer'export function Guide() { const at = state('#previews') // The edge bar folds it. Under 900px it opens over // the page, so no separate button is needed. return ( <div onClick={follow(at)}> <SideNav label="Guide" sections={GUIDE} current={at.value} remember="toolkit:sidenav-guide" /> </div> )}const GUIDE = [ { title: 'Start here', pinned: true, items: [{ href: '#guide', label: 'Overview' }], }, { title: 'Projects', items: [ { href: '#create', label: 'Create a project' }, { href: '#invite', label: 'Invite your team' }, { href: '#archive', label: 'Archive a project' }, ], }, { title: 'Deploys', items: [ { href: '#first-deploy', label: 'Your first deploy', }, { href: '#previews', label: 'Preview deploys' }, { href: '#rollback', label: 'Roll back' }, ], }, { title: 'Billing', items: [ { href: '#plans', label: 'Plans' }, { href: '#invoices', label: 'Invoices' }, ], },]const follow = (at: { value: string }) => (e: MouseEvent) => { const a = (e.target as Element).closest?.('a') if (!a) return e.preventDefault() at.value = a.getAttribute('href') ?? at.value }Installation
npm install @kerf-js/veneerUsage
tsx
import { SideNav } from '@kerf-js/veneer'With a filter
filter adds a box that keeps the matching pages and opens every section they are in. Use it for long lists.
When to use
Use it for
- The pages of a docs site, a guide or a component library
- A list of pages long enough that a reader needs to see where they are in it
- On a phone too: it stays as a bar down the left edge, beside the HamburgerMenu that holds the top links
Reach for something else
- Nav, when there are a handful of links and no sections
- HamburgerMenu, when the links are the site's top links on a phone
- Shell, when it is an app's own navigation, with an account menu
- Accordion, when the sections hold content, not links
Accessibility
- Tab
- Moves between section headings, links and the bar; on a phone it stays inside the open panel
- EnterorSpace
- Opens or closes a section; on the bar, folds or unfolds the sidebar, or opens or closes the panel on a phone
- Escape
- Closes the panel on a phone
labelnames the navigation landmark and titles the panel.- Each section heading is a button with
aria-expandedandaria-controls. A folded section ishidden, so its links leave the tab order. - The current page's link has
aria-current="page". - The bar is a button named by
label, witharia-controlsnaming the panel andaria-expandedsaying whether the sidebar is unfolded, or on a phone whether the panel is open. - On a phone the open panel is a modal dialog labelled by its title: focus moves into it, stays inside, and goes back to the bar when it closes.
Theming
Restyle it app-wide from veneer() in your kerf config. Every key takes colour, spacing, size, radius, shadow, type and motion; its own keys are listed. A state nests inside: hover: { bg: 'ink' }. Text on a fill you set is contrast-checked at build. How theming works
| Key | Own keys | States |
|---|---|---|
parts.close | — | hover, focus, press, active, open, disabled |
parts.nav | — | none |
parts.nav.item | — | hover, focus, press, active, open, disabled |
parts.nav.underline | color, width, duration | none |
ts
veneer({ colors: { brand: '#e8590c', ink: '#1b1b1f' }, parts: { close: { size: 28 }, nav: { gap: 24, item: { hover: { text: 'brand' } }, underline: { color: 'brand', width: 3 } }, },})API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The name of the navigation landmark, and the panel's title on a phone. |
sections | SideNavSection[] | — | The sections, in order: title, items (href and label), and pinned to keep one always open with a plain heading. |
current | string | — | The current page's href. Its link is marked, its section opens, and the rail scrolls to it. |
multiple | boolean | false | Lets any number of sections stay open. By default one is open at a time: opening a section folds the others, and arriving on a page opens its own. |
filter | string | boolean | — | Adds a filter box. A string is its placeholder. |
remember | string | false | — | The localStorage key for a reader's folds, and <key>:collapsed for whether they folded the sidebar. false keeps nothing. Default veneer:sidenav:<label>. |
collapsible | boolean | false | The collapse bar at the panel's edge. Default true. false drops it, and the panel then opens on a phone only from a button whose aria-controls names its id, with a close button in its head. |
id | string | — | The panel's id. A button anywhere with aria-controls set to it also opens the panel on a phone. |
overlay | "cover" | "under" | — | On a phone, where the open panel sits. cover (default) lays it over everything, header included. under opens it below the header (--veneer-sidenav-top) with no title of its own: put a SideNavTitle around the brand's word in the header, and that word gives way to the panel's title while it is open. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="sidenav"] [data-part="backdrop"]
backdroppanelheadtitleclosescrollfilteremptybarbar-label
RelatedNavShellDrawerHamburger menu