Qure UI
Components

Nav List

A set of destinations with one of them current — a side rail, or a row across the top.

'use client'import { useState } from 'react'import { ClipboardListIcon, LayoutGridIcon, SettingsIcon, UsersIcon } from 'lucide-react'import { NavList, NavListItem } from '@/registry/qure/ui/nav-list'const ITEMS = [  { id: 'worklist', label: 'Worklist', icon: <LayoutGridIcon /> },  { id: 'reports', label: 'Reports', icon: <ClipboardListIcon /> },  { id: 'patients', label: 'Patients', icon: <UsersIcon /> },  { id: 'settings', label: 'Settings', icon: <SettingsIcon /> },]export default function NavListDemo() {  const [current, setCurrent] = useState('reports')  return (    <NavList aria-label="Primary" className="w-56">      {ITEMS.map((item) => (        <NavListItem          key={item.id}          href="#"          icon={item.icon}          active={current === item.id}          onClick={(e) => {            e.preventDefault()            setCurrent(item.id)          }}        >          {item.label}        </NavListItem>      ))}    </NavList>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/nav-list.json

Usage

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 doesUse
Changes the URLNavList
Swaps a panel already on the pageTabs
Sets a value, like a units toggleToggleGroup

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

'use client'import { useState } from 'react'import {  BuildingIcon, ClipboardListIcon, LayoutGridIcon, ShieldIcon, UsersIcon,} from 'lucide-react'import { NavList, NavListGroup, NavListItem } from '@/registry/qure/ui/nav-list'export default function NavListGroups() {  const [current, setCurrent] = useState('worklist')  const item = (id: string, label: string, icon: React.ReactNode) => (    <NavListItem      key={id}      href="#"      icon={icon}      active={current === id}      onClick={(e) => {        e.preventDefault()        setCurrent(id)      }}    >      {label}    </NavListItem>  )  return (    <NavList aria-label="Primary" className="w-56">      <NavListGroup label="Reading">        {item('worklist', 'Worklist', <LayoutGridIcon />)}        {item('reports', 'Reports', <ClipboardListIcon />)}      </NavListGroup>      <NavListGroup label="Administration">        {item('users', 'Users and roles', <UsersIcon />)}        {item('sites', 'Sites', <BuildingIcon />)}        {item('audit', 'Audit log', <ShieldIcon />)}      </NavListGroup>    </NavList>  )}

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

'use client'import { useState } from 'react'import { NavList, NavListItem } from '@/registry/qure/ui/nav-list'const ITEMS = ['Overview', 'Findings', 'History']export default function NavListHorizontal() {  const [current, setCurrent] = useState('Findings')  return (    <NavList orientation="horizontal" aria-label="Study sections" className="w-full max-w-md">      {ITEMS.map((label) => (        <NavListItem          key={label}          href="#"          active={current === label}          onClick={(e) => {            e.preventDefault()            setCurrent(label)          }}        >          {label}        </NavListItem>      ))}    </NavList>  )}

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

'use client'import { FileTextIcon, LayoutGridIcon } from 'lucide-react'import { NavList, NavListItem } from '@/registry/qure/ui/nav-list'const SIZES = ['sm', 'md', 'lg'] as constexport default function NavListSize() {  return (    <div className="flex flex-wrap gap-8">      {SIZES.map((size) => (        <NavList key={size} size={size} aria-label={`Primary, ${size}`} className="w-44">          <NavListItem href="#" icon={<LayoutGridIcon />} active>            Worklist          </NavListItem>          <NavListItem href="#" icon={<FileTextIcon />}>            Reports          </NavListItem>        </NavList>      ))}    </div>  )}

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

Renders a <nav> around a <ul>.

PropTypeDefaultDescription
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.
dividerbooleantrueA hairline on the content edge — right when vertical, bottom when horizontal.

Renders an <a> inside an <li>.

PropTypeDefaultDescription
activebooleanfalseSets aria-current="page", which is also what the stylesheet selects on.
iconReactNodeA leading glyph. Pass it unstyled; the stylesheet sizes and colours it.
renderReactElement<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.

PropTypeDefaultDescription
labelReactNodeThe section heading. Rendered in Label Caps and wired to the nested list.

On this page