Nav List
A set of destinations with one of them current — a side rail, or a row across the top.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/nav-list.jsonUsage
import { NavList, NavListGroup, NavListItem } from '@/components/ui/nav-list'<NavList aria-label="Primary">
<NavListItem href="/worklist" icon={<LayoutGridIcon />} active>
Worklist
</NavListItem>
<NavListItem href="/reports" icon={<ClipboardListIcon />}>
Reports
</NavListItem>
</NavList>This is one of the components with no shadcn equivalent. It comes from the Qure design system.
Figma ships six frames — Side navigation and Top navigation, each at small, medium and large. They are one component: three sizes on two axes. The third family, "Toggle navigation", is not a navigation at all; see below.
This is not Tabs
The horizontal variant looks like Tabs, and it must not be Tabs.
A tab reveals a panel that is already in the document. role="tab" is a promise about that: it
tells assistive technology there is a tabpanel with matching aria-controls, that arrow keys
move between them, and that nothing will navigate. A router link keeps none of those promises. Put
role="tab" on one and a screen reader user gets told to expect a panel, presses the arrow keys
expecting to browse, and instead loses the page.
So the markup here is a nav landmark full of anchors, and the current one carries
aria-current="page". The rule is about what the control does, not how it looks:
| What the control does | Use |
|---|---|
| Changes the URL | NavList |
| Swaps a panel already on the page | Tabs |
| Sets a value, like a units toggle | ToggleGroup |
Label the landmark
Give every NavList an aria-label. A real page has several navigation landmarks — the primary
rail, a breadcrumb, something in the footer — and unlabelled they all arrive in the landmark list
as an identical entry called "navigation", which makes the list useless exactly when someone is
relying on it to skip around.
Groups
NavListGroup puts its items in a nested list pointed at the label with aria-labelledby, so the
group is announced as "Administration, list, 3 items". A styled row of small capitals on its own
would look identical and say nothing.
Across the top
The items divide the width evenly rather than sizing to their labels — that is what the Figma frames do, and it keeps the underline positions stable when a label changes length.
Size
Items are 32, 40 and 48px tall. Match the rail to its neighbours: a md rail next to md
buttons.
Routing
render hands the anchor to a router, and active is yours to compute:
const pathname = usePathname()
<NavListItem render={<Link href="/worklist" />} active={pathname === '/worklist'}>
Worklist
</NavListItem>Nothing here derives active from href on its own. Whether /studies/8213 should light up
/studies is a question about your routes, and a component that guessed would be wrong on every
app whose nesting does not match the guess.
The third Figma family
"Toggle navigation" (nodes 3343:7667, 7728, 7729) is a segmented control — a track with a raised
pill on the selected segment — and the library already has it as
ToggleGroup with variant="tray", down to the same 32/40/48
heights.
It is worth being careful with, because the name says navigation and the shape says filter. If the
segments change the URL, this component is the right one and the segmented look can be applied to
it; if they set a value, ToggleGroup already is one.
Where the numbers come from
The six frames are machine-extracted and exact. The tab atoms beside them in the export are
hand-written, and the two disagree: the atom says items are 36/40/48 tall, the frames say
32/40/48. The frames win, and 32 is also what Button and Toggle use at sm.
Type is on the named scale, which costs one step of fidelity. The drawings want weight 500 at 14px and the scale has no such style — it goes 400, then 600. So the current item moves Body Small to Body Small Strong rather than 500 to 600, and is slightly heavier here than in Figma. Easier to pick out at a glance, and nothing invented.
API Reference
NavList
Renders a <nav> around a <ul>.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | A side rail, or a row across the top. |
size | "sm" | "md" | "lg" | "md" | Item heights of 32, 40 and 48px. |
divider | boolean | true | A hairline on the content edge — right when vertical, bottom when horizontal. |
NavListItem
Renders an <a> inside an <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Sets aria-current="page", which is also what the stylesheet selects on. |
icon | ReactNode | — | A leading glyph. Pass it unstyled; the stylesheet sizes and colours it. |
render | ReactElement | <a /> | Hand the anchor to a router link. |
The icon is a prop rather than a child so the label has an element of its own to truncate in. A
bare text node beside an icon in a flex row is an anonymous flex item, and text-overflow has
nothing to bite on — a long name would widen the rail instead of ellipsising.
NavListGroup
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | The section heading. Rendered in Label Caps and wired to the nested list. |