Skip to content
Caseroom

Components · Navigation

Breadcrumb

Where the page sits: a nav with an ordered list and the current page marked.

38% of the 2,062 sites in The UI Index have a navigation (781 sites).

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/breadcrumb.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/breadcrumb.json

Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.

breadcrumb.tsx
import { breadcrumb, SEPARATOR_PATH, type Crumb } from './breadcrumb.variants';

export type { Crumb };

export interface BreadcrumbProps {
  /** From the top down; the last one is the current page (its href is ignored). */
  items: Crumb[];
  label?: string;
  className?: string;
}

/** A breadcrumb trail: nav > ol, the current page with aria-current, separators in CSS. */
export function Breadcrumb({ items, label = 'Breadcrumb', className }: BreadcrumbProps) {
  return (
    <nav aria-label={label} className={className}>
      <ol className={breadcrumb.list}>
        {items.map((c, k) => (
          <li key={k} className={breadcrumb.item}>
            {k === items.length - 1 ? <span aria-current="page" className={breadcrumb.current}>{c.label}</span> : <a href={c.href} className={breadcrumb.link}>{c.label}</a>}
            {k < items.length - 1 && (
              <span className={breadcrumb.separator}>
                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={SEPARATOR_PATH} /></svg>
              </span>
            )}
          </li>
        ))}
      </ol>
    </nav>
  );
}
breadcrumb.variants.ts
/** Where the page sits: a nav with an ordered list, the current page last with aria-current. The separators are
 *  hidden from screen readers (aria-hidden). */
export const breadcrumb = {
  list: 'm-0 flex list-none flex-wrap items-center gap-x-1.5 gap-y-1 p-0 text-sm text-muted-foreground',
  // The separator belongs to the crumb before it, so a wrapped trail never starts a line with one.
  item: 'inline-flex items-center gap-1.5 whitespace-nowrap',
  separator: 'inline-flex text-muted-foreground/70 [&_svg]:size-3.5',
  link: '-mx-1 inline-flex min-h-6 items-center rounded-sm px-1 transition-colors hover:text-foreground hover:underline hover:underline-offset-4 active:text-foreground/70',
  current: 'font-medium text-foreground',
};

export type Crumb = { label: string; href?: string };
export const SEPARATOR_PATH = 'M6 3.5l4.5 4.5L6 12.5';