Skip to content
Folio

Navigation

NavItem

A row in a sidebar.

Usage

TSX
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

NavItem anatomy
ElementDescription
RowrequiredAn <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.
IconAn 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.
Labelrequiredchildren, and the row’s whole accessible name — the icon contributes nothing to it.
Active groundWhat 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.

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.