Skip to content
Navigation & Steps primitive

Breadcrumbs

The trail back up, with the current page as text rather than a link to itself, and a long middle that collapses to a button that announces what it hides and hands focus to the first revealed crumb.

106 lineslucide-react
  • breadcrumb
  • trail
  • navigation
  • path
  • hierarchy

What's included

  • components/ui/breadcrumbs.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add breadcrumbs

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Live

Interactive, in your current theme. Every state below is the real component — type in it, tab through it, switch the theme.

Source

One file. Create components/ui/breadcrumbs.tsx and paste.

components/ui/breadcrumbs.tsx
'use client'

/**
 * <Breadcrumbs> — where you are, as a trail back up.
 *
 *  - A `<nav aria-label="Breadcrumb">` around an ordered list. The list is
 *    what tells a screen reader "list, 4 items"; the ordering is what makes
 *    the trail a sequence and not a pile of links.
 *  - The last crumb is the current page: plain text with `aria-current="page"`,
 *    not a link to itself. A link you can follow to the page you are already
 *    on is a small lie repeated on every page of a site.
 *  - Long trails collapse the middle to a "…" button, and the button is a real
 *    button: it announces how many levels it hides, and on activation it
 *    reveals them AND moves focus to the first revealed crumb. Removing the
 *    button you just pressed without moving focus drops a keyboard user back
 *    at the top of the page.
 *  - The separator is decoration. It is `aria-hidden`, so the trail is not
 *    read as "Home, chevron, Products, chevron", and it mirrors in a
 *    right-to-left document because it points along the reading direction.
 */

import * as React from 'react'
import { ChevronRight } from 'lucide-react'

export interface Crumb {
  label: string
  /** Omit on the last crumb, and on any crumb that is not a page. */
  href?: string
}

export interface BreadcrumbsProps {
  items: Crumb[]
  /** Most crumbs shown before the middle collapses. Minimum 3. */
  maxVisible?: number
  label?: string
  className?: string
}

export function Breadcrumbs({
  items,
  maxVisible = 4,
  label = 'Breadcrumb',
  className = '',
}: BreadcrumbsProps) {
  const [expanded, setExpanded] = React.useState(false)
  const firstRevealed = React.useRef<HTMLAnchorElement | null>(null)
  const limit = Math.max(3, maxVisible)
  const collapsed = !expanded && items.length > limit

  // Keep the first crumb and the last (limit - 2), which is the trail a person
  // most wants: the root, and the few levels nearest where they are.
  const tail = limit - 2
  const hidden = collapsed ? items.slice(1, items.length - tail) : []
  const shown = collapsed ? [items[0], null, ...items.slice(items.length - tail)] : items

  const wasCollapsed = React.useRef(collapsed)
  React.useEffect(() => {
    if (wasCollapsed.current && !collapsed) firstRevealed.current?.focus()
    wasCollapsed.current = collapsed
  }, [collapsed])

  return (
    <nav aria-label={label} className={className}>
      <ol className="flex flex-wrap items-center gap-1.5 text-sm">
        {shown.map((crumb, i) => {
          const last = i === shown.length - 1
          const key = crumb ? `${crumb.label}-${i}` : 'collapsed'
          return (
            <li key={key} className="flex min-w-0 items-center gap-1.5">
              {crumb === null ? (
                <button
                  type="button"
                  onClick={() => setExpanded(true)}
                  aria-label={`Show ${hidden.length} hidden ${hidden.length === 1 ? 'level' : 'levels'}`}
                  className="rounded px-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
                >
                  …
                </button>
              ) : last ? (
                <span aria-current="page" className="truncate font-medium text-foreground" title={crumb.label}>
                  {crumb.label}
                </span>
              ) : (
                <a
                  ref={expanded && i === 1 ? firstRevealed : undefined}
                  href={crumb.href ?? '#'}
                  className="truncate rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
                  title={crumb.label}
                >
                  {crumb.label}
                </a>
              )}
              {last ? null : (
                <ChevronRight
                  aria-hidden
                  className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70 rtl:rotate-180"
                />
              )}
            </li>
          )
        })}
      </ol>
    </nav>
  )
}

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
itemsrequiredCrumb[]—
maxVisibleMost crumbs shown before the middle collapses. Minimum 3.number4
labelstring'Breadcrumb'
classNamestring''

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

More navigation & steps

View all
Open the full page for this primitive

Progress Steps

An ordered list in a nav, not a row of divs — so the count and the position come free, and aria-current names the step you are on. Horizontal and vertical, because four labels do not fit on a phone.

Navigation & Steps145 lines1 dep
Open the full page for this primitive

Tree View

One tab stop for the whole tree, with the ARIA tree keyboard contract in full — including typeahead and the arrow keys mapped through the reading direction.

Navigation & Steps235 lines1 dep
Open the full page for this primitive

Tabs

One tab stop for the whole list, arrow keys mapped through the reading direction, and a manual-activation mode so arrowing past five expensive panels does not fire four requests nobody asked for.

Navigation & Steps181 linesNo deps