Navigation
SidebarNav
A navigation tree of any depth, with persisted expansion, a collapsed icon rail and aria-current on the current page and each of its ancestors. It renders a <nav> and nothing else, so it drops inside the aside your app shell already owns
without adding a second landmark.
Basic
The active row comes from activePath, which you pass in as a pathname. The kit cannot
import your router, so nothing here reads the URL on its own.
#/content/postsWhich row is current
A leaf answers for its own path. A node with children answers for its whole subtree, which is what
lets a section say "you are somewhere in here" while its child says "you are here". So /content/posts marks the Posts row as the page and Content as an ancestor, and two
sibling leaves whose paths share a prefix never mark themselves together. Set match on a node to override that, or to 'none' to opt it out of path matching
entirely.
One row carries the mark at a time. While the section that holds the current page is open, the page's row is marked and the section shows its label in full text color with no bar of its own. Close that section and the section carries the mark until you open it again. A nested row is drawn exactly like a top-level row at rest, under the pointer, on focus and as the current page, with its fill and bar starting at its own indent.
A colored badge reads at 4.5:1 or better on every row: at rest, on the current page's row, and on a row under the pointer or a finger.
Collapsed rail
The rail keeps every label in the accessible name, so a row whose only visible content is an icon is still announced by name rather than as an empty link.
A row that is both a link and a group stacks its disclosure under its link, so the pair fits the rail's width and the rail never scrolls sideways. In the rail below, that is the database icon with its chevron under it. Under a touch screen the link and the disclosure each grow to the 44px control height.
Remembering what the reader closed
With a storageKey, a group the reader opens or closes stays that way across
navigations and reloads. A reader decision always beats the default, so a group they closed does
not reopen itself every time they move inside it. Without a key the same rule applies in memory
for the life of the page. exclusive keeps one group open at a time.
API
| Prop | Type | Default | Description |
|---|---|---|---|
items required | NavTree | no default | The navigation tree. |
activePath required | string | no default | Pathname only, no query and no hash. Passed in, because the kit cannot import your router. |
label | string | 'Primary' | Accessible name of the nav landmark. |
collapsed | boolean | false | Icon-only rail. Labels stay in the accessible name. |
expandActive | boolean | true | Open the ancestors of the current page. |
exclusive | boolean | false | Keep one group open at a time. |
storageKey | string | no default | localStorage key for what the reader opened and closed. Undefined keeps it in memory. |
class | string | no default | Extra classes on the nav element. |
NavNode
| Prop | Type | Default | Description |
|---|---|---|---|
id required | string | no default | Stable across renders. aria-controls is built from it and the list is keyed by it. |
label required | string | no default | Row text. |
href | string | no default | Destination. A node without one is a section, not a link. A javascript:, data: or vbscript: URL is refused, and the row renders without a link. |
icon | Component | no default | A lucide component, drawn before the label. |
children | NavNode[] | no default | Nested rows. Any depth. |
badge | string | number | no default | A count or a status beside the label. |
badgeTone | 'neutral' | 'brand' | 'success' | 'warn' | 'danger' | 'violet' | 'neutral' | Badge color. Its type is AccentTone. |
defaultExpanded | boolean | no default | Open on first render. |
match | 'exact' | 'prefix' | 'none' | no default | How activePath matches. Defaults to 'exact' for a leaf and 'prefix' for a node with children. |
disabled | boolean | false | Render the row as text rather than a link, so it is neither clickable nor focusable. |