# Breadcrumb

Long trails fold their middle into “…”. Click it and the hidden crumbs unblur into place one after another.

**Category:** Navigation  
**Exports:** `Breadcrumb`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/breadcrumb.json
```
The file lands at `components/ui/breadcrumb.tsx`; import from `@/components/ui/breadcrumb`.

## API — Breadcrumb
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `items` | `{ label: string; href?: string }[]` | — |  |
| `maxItems` | `number` | `4` |  |
| `onNavigate` | `(item, event) => void` | — |  |

## Example — Breadcrumb

```tsx
import { Breadcrumb } from "@/components/ui/breadcrumb";

export default function BreadcrumbDemo({ maxItems = 4 }: { maxItems?: number }) {
  return (
    <Breadcrumb
      maxItems={maxItems}
      onNavigate={(_, e) => e.preventDefault()}
      items={[
        { label: "Workspace", href: "#" },
        { label: "Design", href: "#" },
        { label: "Components", href: "#" },
        { label: "Inputs", href: "#" },
        { label: "Range Slider" },
      ]}
    />
  );
}
```

## Source — components/ui/breadcrumb.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type Crumb = { label: string; href?: string };

export type BreadcrumbProps = {
  items: Crumb[];
  /** Collapse the middle when there are more items than this. */
  maxItems?: number;
  onNavigate?: (item: Crumb, event: React.MouseEvent) => void;
  className?: string;
};

function Sep() {
  return (
    <svg viewBox="0 0 24 24" className="size-3.5 shrink-0 text-muted-foreground/60" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
      <path d="m9 6 6 6-6 6" />
    </svg>
  );
}

export function Breadcrumb({ items, maxItems = 4, onNavigate, className }: BreadcrumbProps) {
  const reduce = useReducedMotion();
  const [expanded, setExpanded] = React.useState(false);
  const collapse = !expanded && items.length > maxItems;
  const hidden = collapse ? items.slice(1, items.length - (maxItems - 2)) : [];
  const shown = collapse ? [items[0], ...items.slice(items.length - (maxItems - 2))] : items;

  return (
    <nav aria-label="Breadcrumb" className={className}>
      <ol className="flex flex-wrap items-center gap-1.5 text-sm">
        <AnimatePresence initial={false} mode="popLayout">
          {shown.flatMap((item, i) => {
            const last = item === items[items.length - 1];
            const realIndex = items.indexOf(item);
            const nodes = [
              <motion.li
                key={`${item.label}-${realIndex}`}
                layout={!reduce}
                initial={{ opacity: 0, x: reduce ? 0 : -8, filter: "blur(3px)" }}
                animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
                exit={{ opacity: 0 }}
                transition={{ type: "spring", stiffness: 500, damping: 36, delay: expanded && !reduce ? Math.max(0, realIndex - 1) * 0.04 : 0 }}
                className="flex items-center gap-1.5"
              >
                {last ? (
                  <span aria-current="page" className="px-1 font-medium text-foreground">
                    {item.label}
                  </span>
                ) : (
                  <a
                    href={item.href ?? "#"}
                    onClick={(e) => onNavigate?.(item, e)}
                    className="rounded-md px-1 text-muted-foreground underline-offset-4 outline-none transition-colors hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-ring"
                  >
                    {item.label}
                  </a>
                )}
                {!last && <Sep />}
              </motion.li>,
            ];
            if (i === 0 && collapse) {
              nodes.push(
                <motion.li key="ellipsis" layout className="flex items-center gap-1.5" exit={{ opacity: 0, scale: 0.6 }}>
                  <button
                    type="button"
                    onClick={() => setExpanded(true)}
                    aria-label={`Show ${hidden.length} more`}
                    className="grid h-7 w-8 cursor-pointer place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
                  >
                    …
                  </button>
                  <Sep />
                </motion.li>,
              );
            }
            return nodes;
          })}
        </AnimatePresence>
      </ol>
    </nav>
  );
}
```