# Breadcrumb Collapsed Dropdown — React Breadcrumb Component

> A breadcrumb that folds its middle levels into an ellipsis button; the menu springs open and lists the hidden levels as an indented staircase.

- Category: [Breadcrumbs](https://pixelpapa.com/components/breadcrumbs.md)
- Tags: breadcrumb, dropdown, collapse, menu, navigation
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/breadcrumbs/breadcrumb-collapsed-dropdown

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/breadcrumb-collapsed-dropdown.json"
```

The file is written to `components/pixelpapa/breadcrumb-collapsed-dropdown.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ChevronRight, CornerDownRight, Ellipsis } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
import { cn } from "@/lib/utils";

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

type CollapsedBreadcrumbProps = {
  items: Crumb[];
  /** Crumbs kept visible after the first one. */
  tail?: number;
  defaultOpen?: boolean;
  className?: string;
};

const link =
  "rounded-md px-1.5 py-1 text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60";

export function CollapsedBreadcrumb({ items, tail = 2, defaultOpen = false, className }: CollapsedBreadcrumbProps) {
  const id = useId();
  const [open, setOpen] = useState(defaultOpen);
  const root = useRef<HTMLLIElement>(null);
  const trigger = useRef<HTMLButtonElement>(null);
  const menu = useRef<HTMLDivElement>(null);
  const viaKeyboard = useRef(false);

  const collapse = items.length > tail + 2;
  const hidden = collapse ? items.slice(1, items.length - tail) : [];
  const visible = collapse ? items.slice(items.length - tail) : items.slice(1);

  useEffect(() => {
    if (!open) return;
    if (viaKeyboard.current) menu.current?.querySelector<HTMLElement>("a")?.focus();
    const away = (e: PointerEvent) => {
      if (!root.current?.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener("pointerdown", away);
    return () => document.removeEventListener("pointerdown", away);
  }, [open]);

  function onMenuKey(e: KeyboardEvent<HTMLDivElement>) {
    const links = [...(menu.current?.querySelectorAll<HTMLElement>("a") ?? [])];
    const i = links.indexOf(document.activeElement as HTMLElement);
    if (e.key === "ArrowDown") links[(i + 1) % links.length]?.focus();
    else if (e.key === "ArrowUp") links[(i - 1 + links.length) % links.length]?.focus();
    else if (e.key === "Escape") {
      setOpen(false);
      trigger.current?.focus();
    } else return;
    e.preventDefault();
  }

  const sep = <ChevronRight aria-hidden className="size-3.5 shrink-0 text-muted-foreground/50" />;

  return (
    <nav aria-label="Breadcrumb" className={className}>
      <ol className="flex items-center gap-1 text-sm">
        <li>
          <a href={items[0].href ?? "#"} className={link}>
            {items[0].label}
          </a>
        </li>
        {collapse && (
          <>
            {sep}
            <li ref={root} className="relative">
              <button
                ref={trigger}
                type="button"
                aria-haspopup="menu"
                aria-expanded={open}
                aria-controls={id}
                aria-label={`Show ${hidden.length} hidden levels`}
                onClick={(e) => {
                  viaKeyboard.current = e.detail === 0;
                  setOpen((o) => !o);
                }}
                className={cn(
                  "flex h-7 w-8 items-center justify-center rounded-md border text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60",
                  open ? "border-border bg-muted text-foreground" : "border-transparent",
                )}
              >
                <Ellipsis className="size-4" />
              </button>
              <AnimatePresence>
                {open && (
                  <motion.div
                    ref={menu}
                    id={id}
                    role="menu"
                    onKeyDown={onMenuKey}
                    initial={{ opacity: 0, scale: 0.92, y: -6 }}
                    animate={{ opacity: 1, scale: 1, y: 0 }}
                    exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.1 } }}
                    transition={{ type: "spring", stiffness: 520, damping: 32 }}
                    className="absolute top-full left-0 z-20 mt-2 min-w-52 origin-top-left rounded-xl border border-border bg-popover p-1 text-popover-foreground shadow-xl shadow-black/10"
                  >
                    {hidden.map((c, i) => (
                      <motion.a
                        key={c.label}
                        role="menuitem"
                        href={c.href ?? "#"}
                        onClick={() => setOpen(false)}
                        initial={{ opacity: 0, x: -8 }}
                        animate={{ opacity: 1, x: 0 }}
                        transition={{ delay: 0.03 + i * 0.045, duration: 0.2 }}
                        style={{ paddingLeft: 10 + i * 14 }}
                        className="flex h-8 items-center gap-2 rounded-lg pr-3 whitespace-nowrap outline-none hover:bg-accent focus-visible:bg-accent"
                      >
                        {i > 0 && <CornerDownRight aria-hidden className="size-3.5 text-muted-foreground/60" />}
                        {c.label}
                      </motion.a>
                    ))}
                  </motion.div>
                )}
              </AnimatePresence>
            </li>
          </>
        )}
        {visible.map((c, i) => {
          const current = i === visible.length - 1;
          return (
            <li key={c.label} className="flex min-w-0 items-center gap-1">
              {sep}
              {current ? (
                <span aria-current="page" className="truncate px-1.5 py-1 font-medium text-foreground">
                  {c.label}
                </span>
              ) : (
                <a href={c.href ?? "#"} className={cn(link, "truncate")}>
                  {c.label}
                </a>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

const trail: Crumb[] = [
  { label: "Home" },
  { label: "Workspaces" },
  { label: "Halden Freight" },
  { label: "Projects" },
  { label: "Fleet dashboard" },
  { label: "Alert rules" },
];

export default function BreadcrumbCollapsedDropdownDemo() {
  return (
    <div className="w-full max-w-xl rounded-2xl border border-border bg-card p-6 pb-28 shadow-sm">
      <CollapsedBreadcrumb items={trail} defaultOpen />
      <h3 className="mt-4 text-2xl font-semibold tracking-tight text-card-foreground">Alert rules</h3>
      <p className="mt-1 text-sm text-muted-foreground">Notify dispatch when a vehicle idles for more than 20 minutes.</p>
    </div>
  );
}
```

## AI prompt

```text
Add the "Breadcrumb Collapsed Dropdown" component from PixelPapa UI to this project.

What it is: A breadcrumb that folds its middle levels into an ellipsis button; the menu springs open and lists the hidden levels as an indented staircase.
Category: Breadcrumbs. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/breadcrumb-collapsed-dropdown.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/breadcrumb-collapsed-dropdown.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/breadcrumb-collapsed-dropdown" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More breadcrumbs

- [Breadcrumb Icons Chevrons](https://pixelpapa.com/components/breadcrumbs/breadcrumb-icons-chevrons.md): An icon breadcrumb that assembles itself on mount, each crumb sliding out from behind its chevron, with a hover highlight that glides between items.
- [Breadcrumb Path Editable](https://pixelpapa.com/components/breadcrumbs/breadcrumb-path-editable.md) (Pro): A file-explorer breadcrumb: click the bar and the crumbs turn into a typed path; Enter navigates, a path that does not exist shakes, Escape restores the crumbs.
- [Breadcrumb Steps Progress](https://pixelpapa.com/components/breadcrumbs/breadcrumb-steps-progress.md): A breadcrumb of interlocking arrow segments for linear flows: colour sweeps into the next arrow as you advance and finished steps swap their number for a check.
