Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
items{ label: string; href?: string }[]The trail from the root to the current page, in order. The last item is the page.
maxItemsnumber4Above this many items the middle collapses into a menu. Minimum 2.
separatorReactNode<ChevronRightIcon />Between items. Defaults to the breadcrumb primitive's chevron.
moreLabelstring"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