# Breadcrumb

An accessible React breadcrumb for Next.js and Tailwind CSS: a trail of links whose levels animate as you move, with custom separators and a collapsed middle.

Source: https://ui.fewtone.com/components/breadcrumb

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/breadcrumb.json
```

## Usage

```tsx
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import Link from "next/link"

export function Trail() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink asChild>
            <Link href="/">Home</Link>
          </BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbLink asChild>
            <Link href="/components">Components</Link>
          </BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}
```

## API reference

### Breadcrumb

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `aria-label` | `string` | `"Breadcrumb"` | The landmark's name. |

### BreadcrumbList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### BreadcrumbItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### BreadcrumbLink

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child, for example Next.js Link, with the link's styles. |

### BreadcrumbPage

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### BreadcrumbSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | `a chevron` | Your own mark, such as a slash icon. A chevron flips in right-to-left pages. |

### BreadcrumbEllipsis

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

**When to use it:** Use Breadcrumb at the top of a page deep in a hierarchy (docs, settings, file folders) to show where it sits and link back up. Skip it on flat sites. Collapse the middle into a Dropdown Menu when a trail is long.
**Built on:** Radix Slot.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/
**npm packages (installed by the CLI):** lucide-react@^1.52.0, motion@^14.0.0, radix-ui@^1.7.0.

## Examples

### Trails

Point at a link and its underline fades in. A long trail can fold its middle into a menu: the ellipsis opens a Dropdown Menu of the hidden levels. Pass any icon to BreadcrumbSeparator for a different mark.

```tsx
"use client";

import {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { SlashIcon } from "lucide-react";
import { useId, useState } from "react";

type Mark = "chevron" | "slash";

/** The levels the collapsed trail folds into its menu */
const hidden = [
  { label: "Docs", href: "/docs" },
  { label: "Installation", href: "/docs/installation" },
];

/*
 * A four-level trail and a deeper one with its middle collapsed into a menu, named underneath, with the
 * separator in the top-right corner. An empty row at the bottom, as tall as the setting's row, keeps them centred.
 */
export default function BreadcrumbTrails() {
  const id = useId();
  const [mark, setMark] = useState<Mark>("chevron");
  const separator = <BreadcrumbSeparator>{mark === "slash" ? <SlashIcon /> : undefined}</BreadcrumbSeparator>;

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-separator`} className="text-sm text-muted-foreground">
          Separator
        </label>
        <Select value={mark} onValueChange={(value) => setMark(value as Mark)}>
          <SelectTrigger id={`${id}-separator`} size="sm" className="w-28">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="chevron">Chevron</SelectItem>
            <SelectItem value="slash">Slash</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <ul className="flex flex-1 flex-col items-center justify-center gap-8">
        <li className="flex flex-col items-center gap-2">
          <Breadcrumb aria-label="Breadcrumb, four levels">
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="/">Home</BreadcrumbLink>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <BreadcrumbLink href="/docs">Docs</BreadcrumbLink>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <BreadcrumbLink href="/components">Components</BreadcrumbLink>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
          <span className="text-xs text-muted-foreground">Four levels</span>
        </li>

        <li className="flex flex-col items-center gap-2">
          <Breadcrumb aria-label="Breadcrumb, collapsed">
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="/">Home</BreadcrumbLink>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <DropdownMenu>
                  <DropdownMenuTrigger
                    aria-label="Show hidden levels"
                    className="cursor-pointer rounded-md outline-ring transition-colors duration-(--duration-fast) ease-out hover:text-foreground focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset) data-[state=open]:text-foreground"
                  >
                    <BreadcrumbEllipsis />
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="start">
                    {/* Each hidden level is a real link, rendered as the menu item */}
                    {hidden.map((level) => (
                      <DropdownMenuItem key={level.href} asChild>
                        <a href={level.href}>{level.label}</a>
                      </DropdownMenuItem>
                    ))}
                  </DropdownMenuContent>
                </DropdownMenu>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <BreadcrumbLink href="/components">Components</BreadcrumbLink>
              </BreadcrumbItem>
              {separator}
              <BreadcrumbItem>
                <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
          <span className="text-xs text-muted-foreground">Collapsed</span>
        </li>
      </ul>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### A trail that follows you

Open a folder and the trail grows a level out of the one before it while the list moves on, row by row; press a level above and it plays backwards. Key each level by its path to get this.

```tsx
"use client";

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { blur, duration, ease } from "@/lib/motion";
import { FolderIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion, type Variants } from "motion/react";
import { Fragment, useEffect, useRef, useState } from "react";

type Folder = { name: string; folders?: Folder[] };

const home: Folder = {
  name: "Home",
  folders: [
    {
      name: "Projects",
      folders: [
        {
          name: "FewTone UI",
          folders: [
            { name: "Components", folders: [{ name: "Breadcrumb" }, { name: "Accordion" }] },
            { name: "Docs" },
          ],
        },
        { name: "Website" },
      ],
    },
    { name: "Photos", folders: [{ name: "2026" }, { name: "Travel" }] },
    { name: "Documents" },
  ],
};

/** A level's key: the path down to it, so the same name in two places is two levels */
const keyOf = (path: Folder[], index: number) =>
  path
    .slice(0, index + 1)
    .map((folder) => folder.name)
    .join("/");

/**
 * The folder list moves the way you went (1 deeper, -1 back up): the old list slides 12px away and the new one
 * comes in from the other side, row by row, both through the blur, the way Navigation Menu's panels move.
 */
function useListMotion() {
  const reduce = useReducedMotion() ?? false;
  const shift = reduce ? 0 : 12;
  const hidden = reduce ? blur.shown : blur.hidden;
  const list: Variants = {
    enter: {},
    // Rows arrive one after another, 0.05s apart (motion.md: items appearing in a list)
    center: { transition: { staggerChildren: reduce ? 0 : 0.05 } },
    exit: (direction: number) => ({
      opacity: 0,
      x: -shift * direction,
      filter: hidden,
      transition: reduce ? { duration: 0 } : { duration: duration.fast, ease: ease.in },
    }),
  };
  const row: Variants = {
    enter: (direction: number) => ({ opacity: 0, x: shift * direction, filter: hidden }),
    center: {
      opacity: 1,
      x: 0,
      filter: blur.shown,
      transitionEnd: { filter: "none" },
      transition: reduce ? { duration: 0 } : { duration: duration.slow, ease: ease.out },
    },
  };
  return { list, row };
}

/*
 * A folder browser. Open a folder and the trail grows a level out of the one before it, while the folder list
 * moves on: the old list slides away to the left and the new one comes in from the right, row by row. Press a
 * level above and it all plays backwards: the levels below slide back in and the new list comes from the left.
 * Each level is keyed by its path, which is what lets the trail animate. The old and new lists overlap as they
 * pass (popLayout), in a box as tall as the longest list, so the preview never changes height. Focus moves to
 * the new list (or, in an empty folder, to the level above), because the control that was pressed is gone.
 */
export default function BreadcrumbFolders() {
  const variants = useListMotion();
  const [view, setView] = useState<{ path: Folder[]; direction: number }>({ path: [home], direction: 1 });
  const { path, direction } = view;
  const here = path[path.length - 1] ?? home;
  const trail = useRef<HTMLElement>(null);
  const moved = useRef(false);

  const go = (next: Folder[]) => {
    moved.current = true;
    setView({ path: next, direction: next.length > path.length ? 1 : -1 });
  };

  return (
    <div className="flex w-full max-w-lg flex-col gap-4">
      <Breadcrumb ref={trail} aria-label="Folder">
        <BreadcrumbList>
          {path.map((folder, index) => (
            <Fragment key={keyOf(path, index)}>
              {index > 0 ? <BreadcrumbSeparator /> : null}
              <BreadcrumbItem>
                {index === path.length - 1 ? (
                  <BreadcrumbPage>{folder.name}</BreadcrumbPage>
                ) : (
                  <BreadcrumbLink asChild>
                    <button
                      type="button"
                      className="cursor-pointer"
                      onClick={() => go(path.slice(0, index + 1))}
                    >
                      {folder.name}
                    </button>
                  </BreadcrumbLink>
                )}
              </BreadcrumbItem>
            </Fragment>
          ))}
        </BreadcrumbList>
      </Breadcrumb>

      {/* As tall as the longest list (three folders), so the preview never changes height */}
      <div className="relative min-h-26">
        <AnimatePresence mode="popLayout" initial={false} custom={direction}>
          <motion.div
            key={keyOf(path, path.length - 1)}
            variants={variants.list}
            custom={direction}
            initial="enter"
            animate="center"
            exit="exit"
          >
            <FolderList
              folders={here.folders ?? []}
              label={here.name}
              row={variants.row}
              direction={direction}
              onOpen={(folder) => go([...path, folder])}
              moved={moved}
              trail={trail}
            />
          </motion.div>
        </AnimatePresence>
      </div>
    </div>
  );
}

function FolderList({
  folders,
  label,
  row,
  direction,
  onOpen,
  moved,
  trail,
}: {
  folders: Folder[];
  label: string;
  row: Variants;
  direction: number;
  onOpen: (folder: Folder) => void;
  moved: { current: boolean };
  trail: { current: HTMLElement | null };
}) {
  const list = useRef<HTMLUListElement>(null);

  // Mounted after a move: the pressed control has gone, so focus lands on the first folder here, or in an empty
  // folder on the level above
  useEffect(() => {
    if (!moved.current) return;
    moved.current = false;
    const target =
      list.current?.querySelector("button") ??
      Array.from(trail.current?.querySelectorAll<HTMLElement>("[data-slot=breadcrumb-link]") ?? []).at(-1);
    target?.focus();
  }, [moved, trail]);

  if (folders.length === 0) {
    return (
      <motion.p variants={row} custom={direction} className="px-3 py-2 text-sm text-muted-foreground">
        No folders in {label}.
      </motion.p>
    );
  }

  return (
    <ul ref={list} aria-label={`Folders in ${label}`} className="flex flex-col gap-1">
      {folders.map((folder) => (
        <motion.li key={folder.name} variants={row} custom={direction}>
          <Button variant="ghost" size="sm" className="w-full justify-start" onClick={() => onOpen(folder)}>
            <FolderIcon aria-hidden className="text-muted-foreground" />
            {folder.name}
          </Button>
        </motion.li>
      ))}
    </ul>
  );
}
```

## Related

- [Pagination](https://ui.fewtone.com/components/pagination.md): An accessible React pagination for Next.js and Tailwind CSS: page links with ellipses, Previous and Next, and a pill that slides to the current page.
- [Dropdown Menu](https://ui.fewtone.com/components/dropdown-menu.md): An accessible React dropdown menu for Next.js with a highlight that glides between items, shortcuts, checkbox and radio items and sub-menus, built on Radix.
