Navigation
NavItem
A row in a sidebar.
Usage
import { NavItem } from '@misoto22/design'Notes
A row in a sidebar.
aria-current="page" and not only a colour: the active row has to be identifiable to a reader who cannot see that it is darker. In this system it is also carried by weight and by a filled ground, so it survives monochrome printing and low contrast.
Framework-agnostic. asChild is how a Next or React Router app gets client-side navigation without this package importing either.
Anatomy
| Element | Description |
|---|---|
| Rowrequired | An <a href> at --control-h-sm with the --radius corner — or, under asChild, the router link you handed it, which receives the classes and aria-current and becomes the row itself. |
| Icon | An optional Remix Icon component at 18px, aria-hidden, ahead of the label. It is rendered by the native branch only: a slotted row takes its icon inside the child, because Slot accepts exactly one. |
| Labelrequired | children, and the row’s whole accessible name — the icon contributes nothing to it. |
| Active ground | What active turns on, in one move: a --stone fill, medium weight, and aria-current="page". Three signals, so the current row survives monochrome and low contrast. |
Best practices
Do
- Repeat href on the slotted child: asChild forwards the classes and aria-current and nothing else, so a <Link> that does not carry its own href is a row that navigates nowhere.
- Put the icon inside the child in asChild mode — the icon prop is silently dropped there, which is how a whole sidebar renders as a column of unlabelled-looking rows.
- Derive active from the router’s current path rather than from the last click: it is what writes aria-current="page", so a sidebar tracking its own clicks tells a reader they are on the row they pressed instead of the page they are on.
Don’t
- Do not mark a parent row active to show which section contains the page: active means aria-current="page", and two of them is a reader told they are in two places at once.
- Do not tighten the row further: it is --control-h-sm, which is 36px comfortable and 30px under data-density="compact", and a py class below that leaves a column of targets a thumb has to aim at.
Examples
default
A sidebar column with one current row. active is three signals in one move — a --stone fill, medium weight and aria-current="page" — so the row survives monochrome printing and low contrast, where a colour on its own would not. The icon is aria-hidden and contributes nothing to the name; children are the whole accessible name of the row. Derive active from the router's current path rather than from the last click, or a sidebar tracking its own clicks tells a reader they are on the row they pressed instead of the page they are actually on.
one current row
Two sections, one aria-current between them. Do not also mark the section a page belongs to: active means aria-current="page", and two of them is a reader told they are in two places at once. A group heading is a label rather than a row — these kickers are not focusable and they do not navigate. Do not tighten the rows either; they sit at --control-h-sm, 36px comfortable and 30px under compact density, and a padding class below that leaves a column of targets a thumb has to aim at. The nav is named for the example rather than for what it depicts. This page is itself served out of a nav named "Documentation", and two landmarks of the same role sharing a name cannot be told apart by anyone navigating by landmark — including axe, which fails the page for it.
router link
asChild hands the row to a router's own Link — a plain anchor stands in for one here. It forwards the classes and aria-current and nothing else, so repeat href on the child: a link that does not carry its own href is a row that navigates nowhere. Put the icon inside the child too, because the icon prop is not rendered in this mode at all, and a whole sidebar can go out looking like a column of rows with their icons silently dropped. Slot accepts exactly one child, which is why the icon goes inside the link rather than beside it.
Accessibility
- aria-current="page" and not only a colour: the active row is also carried by weight and a filled ground.