Breadcrumb
The trail from the top of the app to the current page.
shadcn's base-nova breadcrumb with three changes: a link's colour change is timed on the theme's motion tokens (--duration-fast, --ease-standard), so it stops with everything else under reduced motion; a link takes the kit's focus ring, where shadcn's leaves the browser's own; and the default chevron turns in a right-to-left trail, so it points the way the trail reads. The current page is marked aria-current="page".
Install
npx shadcn@latest add @vibra/breadcrumbNeeds the @vibra registry in your components.json — set it up once.
Examples
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
export default function BreadcrumbDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Northwind</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/ecommerce/customers">Customers</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Harbour Logistics</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Collapsed into a menu
A deep path keeps its ends and folds the folders between into a menu that says how many it hides.
import { FolderIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const HIDDEN = [
{ href: "/files/projects", label: "Projects" },
{ href: "/files/projects/q3-launch", label: "Q3 launch" },
{ href: "/files/projects/q3-launch/brand", label: "Brand" },
]
// A deep path keeps its ends: the root, the parent and the page. The folders
// between fold into a menu whose button says how many it hides, and a long
// page name truncates with the whole of it kept in its title.
export default function BreadcrumbCollapsed() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/files">Files</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="ghost" size="icon-xs" aria-label={`Show ${HIDDEN.length} more folders`} />}
>
<BreadcrumbEllipsis />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-44">
{HIDDEN.map((folder) => (
<DropdownMenuItem key={folder.href} render={<a href={folder.href} />}>
<FolderIcon aria-hidden="true" />
{folder.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/files/projects/q3-launch/brand/logos">Logos</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="block max-w-48 truncate" title="northwind-wordmark-horizontal-dark.svg">
northwind-wordmark-horizontal-dark.svg
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Custom separator
The separator's children replace the chevron, and a crumb can be an icon so long as it keeps a name.
import { HouseIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
// The separator's children replace the chevron — a slash here — and a crumb
// can be an icon so long as it keeps a name: the house reads as "Home".
export default function BreadcrumbCustom() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" aria-label="Home">
<HouseIcon aria-hidden="true" className="size-4" />
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/settings">Settings</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/settings/billing">Billing</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Payment methods</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Above a page title
Over a record's title; on a phone it shortens to the one step back.
import { ChevronLeftIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import { StatusBadge } from "@/components/ui/status-badge"
// The trail over a record's title. On a phone it shortens to the one step
// back — the parent, behind a chevron that turns with the reading direction —
// since the title under it already says where the reader is.
export default function BreadcrumbPageHeader() {
return (
<div className="flex w-full max-w-lg flex-col gap-2">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="max-sm:hidden">
<BreadcrumbLink href="/finance">Finance</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem className="max-sm:hidden">
<BreadcrumbLink href="/finance/invoices">Invoices</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbLink href="/finance/customers/blue-harbor" className="inline-flex items-center gap-1">
<ChevronLeftIcon aria-hidden="true" className="size-3.5 sm:hidden rtl:rotate-180" />
Blue Harbor Logistics
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem className="max-sm:hidden">
<BreadcrumbPage>INV-2041</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<h3 className="text-xl font-semibold">INV-2041</h3>
<StatusBadge status="overdue" />
</div>
<Button variant="outline" size="sm">
Send reminder
</Button>
</div>
</div>
)
}With a switcher
Beside the page crumb, a button to go sideways to a sibling without climbing the trail.
"use client"
import * as React from "react"
import { ChevronsUpDownIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const PROJECTS = ["Q3 launch", "Harbour renewal", "Brand refresh", "Data warehouse"]
// The last crumb is where the reader is, and beside it a button to go
// sideways to a sibling without climbing the trail first. The page crumb
// stays the page; the switcher names itself and what it would switch.
export default function BreadcrumbSwitcher() {
const [project, setProject] = React.useState(PROJECTS[0])
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/projects">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{project}</BreadcrumbPage>
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="ghost" size="icon-xs" aria-label={`Switch project (now ${project})`} />}
>
<ChevronsUpDownIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuRadioGroup value={project} onValueChange={setProject}>
<DropdownMenuLabel>Projects</DropdownMenuLabel>
{PROJECTS.map((name) => (
<DropdownMenuRadioItem key={name} value={name}>
{name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Right to left
The demo's trail in Arabic: it runs from the right, and the chevrons turn to point the way it reads.
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
// The demo's trail in Arabic. It runs from the right, and the chevrons turn
// to point the way it reads.
export default function BreadcrumbRtl() {
return (
<Breadcrumb dir="rtl" lang="ar">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Northwind</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/ecommerce/customers">العملاء</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>هاربر للخدمات اللوجستية</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| BreadcrumbLink.render | React.ReactElement | — | Renders the link as your router's link — next/link, say — keeping its look. |
Dependencies
Registry
Source
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
aria-label="breadcrumb"
data-slot="breadcrumb"
className={cn(className)}
{...props}
/>
)
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
function BreadcrumbLink({
className,
render,
...props
}: useRender.ComponentProps<"a">) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
// The kit's one focus treatment: without it a link fell back to the
// browser's thin ring in the accent at half strength.
className: cn(
"rounded-sm transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:text-foreground",
className
),
},
props
),
render,
state: {
slot: "breadcrumb-link",
},
})
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{/* Points the way the trail reads, in either direction. */}
{children ?? <ChevronRightIcon className="rtl:rotate-180" />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn(
"flex size-5 items-center justify-center [&>svg]:size-4",
className
)}
{...props}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}