Qure UI
Components

Breadcrumb

Where you are in a hierarchy, and the way back up it.

'use client'import {  Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,  BreadcrumbPage, BreadcrumbSeparator,} from '@/registry/qure/ui/breadcrumb'export default function BreadcrumbDemo() {  return (    <Breadcrumb>      <BreadcrumbList>        <BreadcrumbItem>          <BreadcrumbLink href="#worklist">Worklist</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbLink href="#patient">Rajan, M.</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbPage>CT Thorax — ACC-100482</BreadcrumbPage>        </BreadcrumbItem>      </BreadcrumbList>    </Breadcrumb>  )}

Installation

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

Usage

import {
  Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,
  BreadcrumbPage, BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/worklist">Worklist</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>CT Thorax — ACC-100482</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

A breadcrumb says where a page sits in a structure that exists whether or not the user arrived through it. It is not a history trail: a radiologist who reached a study from a search still belongs under Worklist › Patient › Study, and showing them the search instead is a lie about the shape of the product. If there is no hierarchy — a flat list of settings pages — there is no breadcrumb.

Composition

Base UI has no breadcrumb primitive, and does not need one. Every affordance here comes from the elements themselves, so the parts exist to get the semantics right rather than to add behaviour.

<Breadcrumb>              {/* <nav aria-label="Breadcrumb"> — names the landmark */}
  <BreadcrumbList>        {/* <ol> — the trail is ordered, and that order is the meaning */}
    <BreadcrumbItem>      {/* <li> — one crumb */}
      <BreadcrumbLink />  {/* <a> — an ancestor you can go to */}
    </BreadcrumbItem>
    <BreadcrumbSeparator/>{/* <li role="presentation" aria-hidden> — a mark, not a crumb */}
    <BreadcrumbItem>
      <BreadcrumbPage />  {/* <span aria-current="page"> — where you are */}
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

Four of those are load-bearing:

PartWhy it is what it is
BreadcrumbA nav with aria-label="Breadcrumb". Without the label it is one of several unnamed navigation landmarks and a screen-reader user cannot tell them apart.
BreadcrumbListAn ol, not a div of links. The order carries the hierarchy, and a list announces its length — "list of five items" tells you how deep you are before you have heard a single crumb.
BreadcrumbPageA span with aria-current="page", not a link. The page you are on is not somewhere you can navigate to, and a link that goes nowhere is a broken promise.
BreadcrumbSeparatorAn li marked role="presentation" and aria-hidden. It stays inside the ol so the HTML is valid, and out of the accessibility tree so the trail reads "Worklist, link. CT Thorax, current page." with nothing between.

BreadcrumbSeparator renders our Separator, turned vertical and cut to 12px, when you give it no children. Pass children to replace it with anything else.

Basic

'use client'import {  Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,  BreadcrumbPage, BreadcrumbSeparator,} from '@/registry/qure/ui/breadcrumb'export default function BreadcrumbDemo() {  return (    <Breadcrumb>      <BreadcrumbList>        <BreadcrumbItem>          <BreadcrumbLink href="#worklist">Worklist</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbLink href="#patient">Rajan, M.</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbPage>CT Thorax — ACC-100482</BreadcrumbPage>        </BreadcrumbItem>      </BreadcrumbList>    </Breadcrumb>  )}

Study navigation: the list, the patient, the study you have open.

Chevron separators

'use client'import { ChevronRightIcon } from 'lucide-react'import {  Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,  BreadcrumbPage, BreadcrumbSeparator,} from '@/registry/qure/ui/breadcrumb'export default function BreadcrumbChevron() {  return (    <Breadcrumb>      <BreadcrumbList>        <BreadcrumbItem>          <BreadcrumbLink href="#reports">Reports</BreadcrumbLink>        </BreadcrumbItem>        {/* Anything passed as children replaces the rule. The li stays            aria-hidden either way, so the icon is never announced. */}        <BreadcrumbSeparator><ChevronRightIcon /></BreadcrumbSeparator>        <BreadcrumbItem>          <BreadcrumbLink href="#drafts">Drafts</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator><ChevronRightIcon /></BreadcrumbSeparator>        <BreadcrumbItem>          <BreadcrumbPage>CXR — ACC-100479</BreadcrumbPage>        </BreadcrumbItem>      </BreadcrumbList>    </Breadcrumb>  )}

A chevron reads as direction where a rule reads as division, which suits a trail that is going somewhere. Anything passed as children replaces the rule; the li stays hidden either way, so the icon is never announced.

Collapsed

'use client'import * as React from 'react'import {  Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink,  BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator,} from '@/registry/qure/ui/breadcrumb'const middle = [  { label: 'Thoracic imaging', href: '#thoracic' },  { label: 'Rajan, M.', href: '#patient' },  { label: 'CT Thorax, 2 April 2025', href: '#study' },]export default function BreadcrumbCollapsed() {  const [expanded, setExpanded] = React.useState(false)  return (    <Breadcrumb>      <BreadcrumbList>        <BreadcrumbItem>          <BreadcrumbLink href="#worklist">Worklist</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        {expanded ? (          middle.map(crumb => (            <React.Fragment key={crumb.href}>              <BreadcrumbItem>                <BreadcrumbLink href={crumb.href}>{crumb.label}</BreadcrumbLink>              </BreadcrumbItem>              <BreadcrumbSeparator />            </React.Fragment>          ))        ) : (          <>            <BreadcrumbItem>              {/* A button, not the ellipsis span alone — three hidden crumbs                  the keyboard cannot reach is a dead end. */}              <button                type="button"                aria-label="Show three hidden crumbs"                aria-expanded={expanded}                onClick={() => setExpanded(true)}                className="cn-breadcrumb-link"              >                <BreadcrumbEllipsis />              </button>            </BreadcrumbItem>            <BreadcrumbSeparator />          </>        )}        <BreadcrumbItem>          <BreadcrumbPage>Series 4 — Axial 1mm</BreadcrumbPage>        </BreadcrumbItem>      </BreadcrumbList>    </Breadcrumb>  )}

Past four or five crumbs the trail stops being scannable. Drop the middle and leave a control that brings it back — a button, not the ellipsis alone, because three crumbs the keyboard cannot reach is a dead end.

With a router

'use client'import Link from 'next/link'import {  Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,  BreadcrumbPage, BreadcrumbSeparator,} from '@/registry/qure/ui/breadcrumb'export default function BreadcrumbRouter() {  return (    <Breadcrumb>      <BreadcrumbList>        {/* render swaps the <a> for the router's link and keeps the class,            so client-side navigation costs nothing in styling. */}        <BreadcrumbItem>          <BreadcrumbLink render={<Link href="/docs" />}>Docs</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbLink render={<Link href="/docs/components/accordion" />}>Components</BreadcrumbLink>        </BreadcrumbItem>        <BreadcrumbSeparator />        <BreadcrumbItem>          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>        </BreadcrumbItem>      </BreadcrumbList>    </Breadcrumb>  )}

render swaps the <a> for a router link and keeps the class and the props:

<BreadcrumbLink render={<Link href="/docs" />}>Docs</BreadcrumbLink>

Do not put a breadcrumb and a back button next to each other doing the same job. The breadcrumb's last-but-one crumb is the back button, and two of them in a header means one of them is wrong the moment the user arrived sideways.

API Reference

Every part takes the props of the element it renders — nav, ol, li, a, span — so href, target and onClick all work as they look. The one addition:

Prop

Type

On this page