# Breadcrumb

> A trail from the root to the current page that folds its middle crumbs into a menu when it runs out of room, measured rather than guessed, with links that underline on hover.

- Tier: Free (MIT)
- Interaction: Press
- Page: https://eased.dev/components/breadcrumb
- Registry: https://eased.dev/r/breadcrumb.json

## Install

```bash
npx shadcn add https://eased.dev/r/breadcrumb.json
```

A trail from the root to the current page that folds its middle into a "…" menu when it runs out of room. The menu is the Eased [Dropdown Menu](/components/dropdown-menu), on Base UI Menu.

## When to use

- Deep hierarchies people move up through: folders, docs sections, a project's settings.
- Page headers where the parent pages are useful shortcuts, not decoration.
- Narrow headers and toolbars: the fold keeps the root and the current page readable at any width.

## When not to use

- Flat sites one level deep: the page title is enough.
- Steps in a sequence: use a stepper or progress indicator.
- Switching between sibling views: use [Tabs](/components/tabs).

## Usage

```tsx doc-header.tsx
import { Breadcrumb } from "@/components/eased/breadcrumb/breadcrumb";

export function DocHeader() {
  return (
    <Breadcrumb
      items={[
        { label: "Acme", href: "/" },
        { label: "Marketing", href: "/marketing" },
        { label: "Website redesign", href: "/marketing/website" },
        { label: "Homepage" },
      ]}
    />
  );
}
```

## Examples

### Your router's link

```tsx
import Link from "next/link";

<Breadcrumb items={items} renderLink={(props) => <Link {...props} />} />
```

### Slash separators, small

```tsx
<Breadcrumb items={items} separator="slash" size="sm" />
```

### Crumbs that act in place

Items with `onClick` and no `href` render as buttons, for file browsers and other in-app navigation.

```tsx
<Breadcrumb items={path.map((name, i) => ({ label: name, onClick: () => openFolder(i) }))} />
```

## API reference

### Breadcrumb

| Prop | Type | Default | Description |
|---|---|---|---|
| `items` | `BreadcrumbItem[]` | | From the root to the current page. The last item is the current page. |
| `separator` | `"chevron" \| "slash" \| ReactNode` | `"chevron"` | |
| `collapse` | `boolean` | `true` | Fold middle crumbs into a "…" menu when the row would overflow. |
| `size` | `"sm" \| "md"` | `"md"` | 13px for toolbars and cards, 14px for page headers. |
| `renderLink` | `(props) => ReactElement` | | Your router's link; it receives `href`, `className`, `title` and `children`. |
| `aria-label` | `string` | `"Breadcrumb"` | The landmark's name. |
| `className` | `string` | `""` | Appended to the `nav`'s classes. |

### BreadcrumbItem

| Field | Type | Description |
|---|---|---|
| `label` | `ReactNode` | The visible label; long labels truncate (ancestors at 12rem). |
| `href` | `string` | Where it goes. Ignored on the last item. |
| `onClick` | `() => void` | For crumbs that act rather than navigate. |
| `icon` | `ReactNode` | A 16px icon before the label. |
| `textValue` | `string` | Plain text for the menu and `title` when `label` is not a string. |

The root is a block `nav`: give it the row's width (`w-full`, or `flex-1 min-w-0` in a flex row) so it can measure what fits.

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Moves through the links and the "…" trigger. |
| `Enter` / `Space` on "…" | Opens the menu with the folded crumbs; arrows move, `Enter` follows, `Esc` closes and returns focus. |

## Accessibility

- A `nav` landmark with an ordered list; the current page has `aria-current="page"` and is not a link.
- Separators are `aria-hidden` presentational items, so screen readers count only crumbs.
- The "…" trigger is named "2 more" (the folded count) and announces its menu.
- Truncated labels keep their full text in `title`.

## Motion

Links underline on hover the way Eased's other links do: the underline is there at rest in a transparent colour and tweens to `line-strong` with the text colour in 150ms, on pointer devices only. The fold itself is instant, because it follows resizing. The menu opens 4px from its trigger at 0.97 in 150ms and leaves in 100ms, with a gliding row highlight. Reduced motion keeps the colour tweens and drops the menu's travel.

## Notes for AI

Use Breadcrumb in a page or panel header for a real hierarchy, passing every level from the root and the current page last. Give it the full row width so the fold can measure; pass `renderLink` for Next.js or another router.

Don't add your own overflow logic, separators between items or `aria-current`: it measures labels, draws separators and marks the current page. Don't put actions in the trail; keep them beside it.

## Related

- [Dropdown Menu](/components/dropdown-menu): the menu the folded crumbs live in.
- [Tabs](/components/tabs): sibling views on one level.

## Example

`components/eased/breadcrumb/breadcrumb-demo.tsx`

```tsx
"use client";

import { useState } from "react";
import { DemoFrame } from "../lib/demo-frame";
import { Breadcrumb, type BreadcrumbItem } from "./breadcrumb";

type Node = { name: string; modified: string; children?: Node[]; size?: string };

const TREE: Node = {
  name: "Acme",
  modified: "",
  children: [
    {
      name: "Marketing",
      modified: "Oct 16",
      children: [
        {
          name: "Website redesign",
          modified: "Oct 17",
          children: [
            {
              name: "Design files",
              modified: "Oct 17",
              children: [
                {
                  name: "Homepage",
                  modified: "Today",
                  children: [
                    { name: "Hero light.fig", modified: "Today", size: "4.2 MB" },
                    { name: "Hero dark.fig", modified: "Today", size: "4.4 MB" },
                    { name: "Pricing table.fig", modified: "Yesterday", size: "1.8 MB" },
                    { name: "Footer.fig", modified: "Oct 14", size: "640 KB" },
                    { name: "Nav.fig", modified: "Oct 15", size: "310 KB" },
                    { name: "Testimonials.fig", modified: "Oct 15", size: "920 KB" },
                    { name: "CTA band.fig", modified: "Oct 13", size: "410 KB" },
                    { name: "Icons.fig", modified: "Oct 9", size: "1.1 MB" },
                  ],
                },
                { name: "Product pages", modified: "Oct 15", children: [{ name: "Overview.fig", modified: "Oct 15", size: "3.1 MB" }] },
                { name: "Brand assets", modified: "Oct 9", children: [{ name: "Logo kit.zip", modified: "Oct 9", size: "12 MB" }] },
              ],
            },
            { name: "Copy deck.docx", modified: "Oct 12", size: "88 KB" },
            { name: "Launch plan.pdf", modified: "Oct 10", size: "1.2 MB" },
          ],
        },
        { name: "Q4 campaign", modified: "Oct 2", children: [{ name: "Brief.pdf", modified: "Oct 2", size: "320 KB" }] },
      ],
    },
    { name: "Finance", modified: "Oct 11", children: [{ name: "Budget 2027.xlsx", modified: "Oct 11", size: "210 KB" }] },
  ],
};

const Folder = () => (
  <svg viewBox="0 0 16 16" className="size-4 shrink-0" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" aria-hidden>
    <path d="M2 4.5A1.5 1.5 0 0 1 3.5 3h2.6l1.4 1.5h5A1.5 1.5 0 0 1 14 6v5.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 11.5v-7Z" />
  </svg>
);

const File = () => (
  <svg viewBox="0 0 16 16" className="size-4 shrink-0" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" aria-hidden>
    <path d="M4 1.75h5L12.5 5.25v8a1 1 0 0 1-1 1h-7.5a1 1 0 0 1-1-1V2.75a1 1 0 0 1 1-1Z M9 1.75v3.5h3.5" />
  </svg>
);

const Logo = () => (
  <span className="flex size-4 items-center justify-center rounded-control-2xs bg-brand text-ui-2xs font-semibold text-text-on-brand">A</span>
);

// Starts five folders deep, so a narrow tile has something to fold.
const START = ["Marketing", "Website redesign", "Design files", "Homepage"];

export function BreadcrumbDemo() {
  const [path, setPath] = useState<string[]>(START);

  const trail: Node[] = [TREE];
  for (const name of path) trail.push(trail.at(-1)!.children!.find((c) => c.name === name)!);
  const here = trail.at(-1)!;

  const items: BreadcrumbItem[] = trail.map((node, i) => ({
    label: node.name,
    icon: i === 0 ? <Logo /> : undefined,
    onClick: () => setPath(path.slice(0, i)),
  }));

  return (
    <DemoFrame>
      <div className="flex h-12 shrink-0 items-center border-b border-line-subtle px-(--pad)">
        <Breadcrumb items={items} aria-label="Folder" className="w-full" />
      </div>
      <ul className="grid min-h-0 flex-1 grid-cols-[1fr_auto] content-start overflow-y-auto px-2 py-2">
        {here.children?.map((node) => {
          const folder = !!node.children;
          const row = (
            <>
              <span className="flex min-w-0 items-center gap-2.5">
                <span className="text-text-muted">{folder ? <Folder /> : <File />}</span>
                <span className="truncate text-ui text-text">{node.name}</span>
              </span>
              <span className="text-end text-ui-sm text-text-muted tabular-nums">{folder ? node.modified : node.size}</span>
            </>
          );
          return (
            <li key={node.name} className="col-span-2 grid grid-cols-subgrid">
              {folder ? (
                <button
                  type="button"
                  onClick={() => setPath([...path, node.name])}
                  className="col-span-2 grid h-10 cursor-pointer grid-cols-subgrid items-center gap-x-4 rounded-control px-2 text-start transition-colors duration-150 ease-standard focus-ring hover-fine:bg-surface-hover active:bg-surface-active"
                >
                  {row}
                </button>
              ) : (
                <div className="col-span-2 grid h-10 grid-cols-subgrid items-center gap-x-4 px-2">{row}</div>
              )}
            </li>
          );
        })}
      </ul>
    </DemoFrame>
  );
}
```
