ComponentsCSS
Breadcrumb
Trail of links where the last item marks the current page.
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/glass/breadcrumb.jsonAlso installs the theme, cn(). Packages: clsx, tailwind-merge.
Usage
breadcrumb-demo.tsx
"use client";
import { useState } from "react";
import { Breadcrumb } from "@/registry/kit/ui/breadcrumb";
const trail = ["acme", "marketing-site", "Deployments", "4f2a91c"];
export default function BreadcrumbDemo() {
// Click a crumb to walk back up the trail; a real app would route here.
const [depth, setDepth] = useState(trail.length);
return (
<div className="grid justify-items-center gap-4">
<div className="surface rounded-control px-5 py-3">
<Breadcrumb items={trail.slice(0, depth).map((label) => ({ label }))} onSelect={(i) => setDepth(i + 1)} />
</div>
{depth < trail.length && (
<button
type="button"
onClick={() => setDepth(trail.length)}
className="cursor-pointer rounded-control text-xs text-muted underline-offset-4 hover:text-fg hover:underline focus-visible:outline-2 focus-visible:outline-accent"
>
Restore full trail
</button>
)}
</div>
);
}API
items{ label; href? }[]From root to current page.
onSelect(index: number) => voidHandle navigation yourself; items without href become buttons.
itemProps(index: number) => HTMLAttributesExtra attributes and handlers per crumb, e.g. to make crumbs drop targets.
Source
components/ui/breadcrumb.tsx
import type { HTMLAttributes } from "react";
import { cn } from "@/registry/kit/lib/utils";
export interface BreadcrumbItem {
label: React.ReactNode;
href?: string;
}
const crumbClass =
"rounded-md text-muted outline-none transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent";
/**
* Trail of links. The last item is the current page and isn't a link.
* Pass `onSelect` to handle navigation yourself (client routing, in-app
* drill-down); items then render as buttons when they have no `href`.
* `itemProps` adds attributes and handlers to each crumb, e.g. to make
* crumbs drop targets in a file browser.
*/
export function Breadcrumb({
items,
onSelect,
itemProps,
className,
}: {
items: BreadcrumbItem[];
onSelect?: (index: number) => void;
itemProps?: (index: number) => HTMLAttributes<HTMLElement> & Record<`data-${string}`, string | boolean | undefined>;
className?: string;
}) {
return (
<nav aria-label="Breadcrumb" className={className}>
<ol className="flex flex-wrap items-center gap-1.5 text-sm">
{items.map((item, i) => {
const last = i === items.length - 1;
return (
<li key={i} className="flex items-center gap-1.5">
{last ? (
<span aria-current="page" {...itemProps?.(i)} className={cn("rounded-md text-fg", itemProps?.(i).className)}>
{item.label}
</span>
) : item.href ? (
<a href={item.href} onClick={onSelect && (() => onSelect(i))} {...itemProps?.(i)} className={cn(crumbClass, itemProps?.(i).className)}>
{item.label}
</a>
) : onSelect ? (
<button type="button" onClick={() => onSelect(i)} {...itemProps?.(i)} className={cn(crumbClass, "cursor-pointer", itemProps?.(i).className)}>
{item.label}
</button>
) : (
<span {...itemProps?.(i)} className={cn("text-muted", itemProps?.(i).className)}>{item.label}</span>
)}
{!last && (
<svg viewBox="0 0 24 24" className="size-3.5 text-fg/30" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
<path d="M9 6l6 6-6 6" />
</svg>
)}
</li>
);
})}
</ol>
</nav>
);
}