Breadcrumb trail
Breadcrumbs that fold the middle levels into an ellipsis menu when the trail is long.
npx shadcn@latest add https://ui.goodtabs.co/r/breadcrumb-trail.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/breadcrumb-trail
Three levels. The last one is the current page in foreground.
Collapsed
Six levels fold to four. The ellipsis opens a menu of the hidden pages.
Slash separator
Any node works as the separator. A slash reads like a file path.
Usage
import { BreadcrumbTrail } from "@/components/ui/breadcrumb-trail"
<BreadcrumbTrail
items={[
{ label: "Home", href: "/" },
{ label: "Projects", href: "/projects" },
{ label: "Northwind" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { label: string; href?: string }[] | The trail from the root to the current page, in order. The last item is the page. | |
| maxItems | number | 4 | Above this many items the middle collapses into a menu. Minimum 2. |
| separator | ReactNode | <ChevronRightIcon /> | Between items. Defaults to the breadcrumb primitive's chevron. |
| moreLabel | string | "Show hidden pages" | Accessible name of the ellipsis button. |
Source
breadcrumb-trail/breadcrumb-trail.tsx
import * as React from "react";
import Link from "next/link";
import { MoreHorizontalIcon } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
export type BreadcrumbTrailItem = {
label: string;
/** Omit on the last item, or on any level that has no page of its own. */
href?: string;
};
type BreadcrumbTrailProps = Omit<React.ComponentProps<"nav">, "children"> & {
/** The trail from the root to the current page, in order. */
items: BreadcrumbTrailItem[];
/** Above this many items the middle collapses into a menu. */
maxItems?: number;
/** Between items. Defaults to a chevron. */
separator?: React.ReactNode;
/** Accessible name of the collapsed menu button. */
moreLabel?: string;
};
/**
* Breadcrumbs that collapse the middle into an ellipsis menu when the trail is long.
* The first item and the last few stay visible; the last one is the current page.
*/
export function BreadcrumbTrail({
items,
maxItems = 4,
separator,
moreLabel = "Show hidden pages",
className,
...props
}: BreadcrumbTrailProps) {
const limit = Math.max(2, maxItems);
const collapsed = items.length > limit;
const tailCount = limit - 2;
const head = collapsed ? items.slice(0, 1) : items;
const hidden = collapsed ? items.slice(1, items.length - tailCount) : [];
const tail = collapsed ? items.slice(items.length - tailCount) : [];
const last = items.length - 1;
const renderItem = (item: BreadcrumbTrailItem, index: number) => (
<BreadcrumbItem key={index}>
{index === last ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
) : item.href ? (
<BreadcrumbLink asChild>
<Link href={item.href} className="rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50">
{item.label}
</Link>
</BreadcrumbLink>
) : (
<span>{item.label}</span>
)}
</BreadcrumbItem>
);
const sep = (key: string) => <BreadcrumbSeparator key={key}>{separator}</BreadcrumbSeparator>;
return (
<Breadcrumb data-slot="breadcrumb-trail" className={cn(className)} {...props}>
<BreadcrumbList>
{head.map((item, i) => (
<React.Fragment key={i}>
{i > 0 && sep(`sep-${i}`)}
{renderItem(item, i)}
</React.Fragment>
))}
{collapsed && (
<>
{sep("sep-more")}
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={moreLabel}
className="flex size-7 items-center justify-center rounded-sm transition-colors duration-100 ease-out outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted aria-expanded:text-foreground"
>
<MoreHorizontalIcon className="size-4" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-auto min-w-40">
{hidden.map((item, i) =>
item.href ? (
<DropdownMenuItem key={i} asChild>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
) : (
<DropdownMenuItem key={i} disabled>
{item.label}
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
{tail.map((item, i) => {
const index = items.length - tailCount + i;
return (
<React.Fragment key={index}>
{sep(`sep-${index}`)}
{renderItem(item, index)}
</React.Fragment>
);
})}
</>
)}
</BreadcrumbList>
</Breadcrumb>
);
}
For AI agents
Hand your agent this page, the markdown at /components/breadcrumb-trail/prompt.md, or the Copy AI prompt button above.
When to use
- At the top of a page that sits three or more levels deep
- In docs, settings and file browsers where the path is the context
- When long trails must stay on one line without losing the hidden levels
When not to use
- On a page one level below home: a single back link is enough
- As the main navigation; it shows where you are, not where you can go
- For tabs or steps in a flow, which are not a hierarchy
Agent notes
- Server component on the breadcrumb and dropdown-menu primitives; links are next/link, so pass real hrefs
- With more than maxItems the first item and the last maxItems minus 2 stay visible; the rest go in the menu
- The last item renders as BreadcrumbPage with aria-current, whether or not it has an href
- A hidden level without an href shows as a disabled menu item