Skip to content

Breadcrumb

A hierarchy trail with a collapsing middle for deep paths and horizontal scroll on mobile.

Navigationstarternavigationhierarchywayfinding

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

The exact file rendered in the preview above.

import { Breadcrumb } from '@/components/ui/breadcrumb'
import { DemoRow, DemoStage } from './_kit'

export default function BreadcrumbDemo() {
  return (
    <DemoStage>
      <DemoRow label="Standard">
        <Breadcrumb
          items={[
            { label: 'Foundry', href: '/' },
            { label: 'Components', href: '/components' },
            { label: 'Breadcrumb' },
          ]}
        />
      </DemoRow>

      <DemoRow label="Deep trail, collapsed">
        <Breadcrumb
          maxItems={4}
          items={[
            { label: 'Foundry', href: '/' },
            { label: 'Starters', href: '/starters' },
            { label: 'Ecommerce', href: '/starters/ecommerce' },
            { label: 'Shop', href: '/starters/ecommerce/shop' },
            { label: 'Outerwear', href: '/starters/ecommerce/category/outerwear' },
            { label: 'Field Shell Jacket' },
          ]}
        />
      </DemoRow>

      <DemoRow label="Two levels">
        <Breadcrumb items={[{ label: 'Docs', href: '/docs' }, { label: 'Design tokens' }]} />
      </DemoRow>
    </DemoStage>
  )
}

components/demos/breadcrumb.tsx

import { Breadcrumb } from '@/components/ui/breadcrumb'
import { DemoRow, DemoStage } from './_kit'

export default function BreadcrumbDemo() {
  return (
    <DemoStage>
      <DemoRow label="Standard">
        <Breadcrumb
          items={[
            { label: 'Foundry', href: '/' },
            { label: 'Components', href: '/components' },
            { label: 'Breadcrumb' },
          ]}
        />
      </DemoRow>

      <DemoRow label="Deep trail, collapsed">
        <Breadcrumb
          maxItems={4}
          items={[
            { label: 'Foundry', href: '/' },
            { label: 'Starters', href: '/starters' },
            { label: 'Ecommerce', href: '/starters/ecommerce' },
            { label: 'Shop', href: '/starters/ecommerce/shop' },
            { label: 'Outerwear', href: '/starters/ecommerce/category/outerwear' },
            { label: 'Field Shell Jacket' },
          ]}
        />
      </DemoRow>

      <DemoRow label="Two levels">
        <Breadcrumb items={[{ label: 'Docs', href: '/docs' }, { label: 'Design tokens' }]} />
      </DemoRow>
    </DemoStage>
  )
}

components/ui/breadcrumb.tsx

import Link from 'next/link'
import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Breadcrumb
 *
 * A `<nav>` wrapping an ordered list. The final crumb is not a link and
 * carries `aria-current="page"`, and the separators are decorative text inside
 * `aria-hidden` spans so a screen reader reads "Components, Button" rather
 * than "Components chevron Button".
 *
 * On narrow screens the trail scrolls horizontally instead of wrapping into an
 * unreadable stack.
 */
export interface Crumb {
  label: string
  href?: string
}

export interface BreadcrumbProps {
  items: Crumb[]
  className?: string
  /** Collapses the middle of very deep trails behind an ellipsis. */
  maxItems?: number
}

export function Breadcrumb({ items, className, maxItems }: BreadcrumbProps) {
  const shouldCollapse = maxItems !== undefined && items.length > maxItems
  const visible: Array<Crumb | 'ellipsis'> = shouldCollapse
    ? [items[0] as Crumb, 'ellipsis', ...items.slice(items.length - (maxItems - 1))]
    : items

  return (
    <nav aria-label="Breadcrumb" className={cn('min-w-0', className)}>
      <ol className="hide-scrollbar flex items-center gap-1 overflow-x-auto text-xs whitespace-nowrap">
        {visible.map((item, index) => {
          const isLast = index === visible.length - 1
          return (
            <li
              key={typeof item === 'string' ? `ellipsis-${index}` : `${item.label}-${index}`}
              className="flex items-center gap-1"
            >
              {index > 0 ? (
                <ChevronRight className="size-3 shrink-0 text-ink-subtle" aria-hidden="true" />
              ) : null}
              {item === 'ellipsis' ? (
                <span className="px-1 text-ink-subtle" aria-hidden="true">
                  …
                </span>
              ) : isLast || !item.href ? (
                <span
                  className={cn('font-medium', isLast ? 'text-ink' : 'text-ink-muted')}
                  aria-current={isLast ? 'page' : undefined}
                >
                  {item.label}
                </span>
              ) : (
                <Link
                  href={item.href}
                  className="rounded-xs text-ink-muted transition-colors duration-150 hover:text-ink"
                >
                  {item.label}
                </Link>
              )}
            </li>
          )
        })}
      </ol>
    </nav>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Only where a real hierarchy exists. A breadcrumb that shows how a user got here rather than where "here" sits in the structure is a history list, and it misleads.

  • The final crumb is the current page and is never a link.
  • On narrow screens the trail scrolls rather than wrapping into an unreadable stack.
  • Collapse the middle of deep trails; the first and last crumbs carry most of the meaning.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Standard trail
  • Collapsed with maxItems
  • Two levels

Accessibility

Landmark
A `<nav>` labelled "Breadcrumb" wrapping an ordered list.
Current page
The last item carries `aria-current="page"`.
Separators
Chevrons are `aria-hidden`, so the trail reads "Components, Button" rather than "Components chevron Button".

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.