import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
export default function BreadcrumbsDemo () {
return (
< Breadcrumbs >
< Breadcrumb href = "#" >Home</ Breadcrumb >
< Breadcrumb href = "#" >Projects</ Breadcrumb >
< Breadcrumb href = "#" >acme-web</ Breadcrumb >
< Breadcrumb >Settings</ Breadcrumb >
</ Breadcrumbs >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/breadcrumbs
The CLI installs dependencies and any other components this one uses.
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
< Breadcrumbs >
< Breadcrumb href = "/" >Home</ Breadcrumb >
< Breadcrumb href = "/projects" >Projects</ Breadcrumb >
< Breadcrumb >Settings</ Breadcrumb >
</ Breadcrumbs >
The last Breadcrumb is the current page. It gets aria-current="page", is rendered as non-interactive text even if it has an href, and has no separator after it.
Breadcrumbs — secondary navigation that shows the path to the current page in a hierarchy two or more levels deep: file systems, product catalogs, settings, docs.
Tabs — sibling views at the same level, not a path.
Link — a single "Back to…" link is often enough for shallow hierarchies and small screens.
Sidebar — primary navigation. Breadcrumbs complement it; they don't replace it.
< Breadcrumbs > { /* <ol> */ }
< Breadcrumb href = "…" icon > { /* <li> › link + separator */ }
Label
</ Breadcrumb >
< Breadcrumb > { /* current page: no link, no separator */ }
Label
</ Breadcrumb >
</ Breadcrumbs >
Part Renders Notes Breadcrumbs<ol>Labelled "Breadcrumbs" by default (localized). Sets the separator style and text size for its items. Wraps onto multiple lines when needed. Breadcrumb<li>One level. Contains a React Aria Link with the optional icon and the label, or the children as-is with link={false}. Link <a>, or <span role="link">An <a> when href is set; a <span> for the current item, disabled items and items without href. Separator <svg> or <span>Chevron, slash or dot after every item except the current one. aria-hidden.
chevron (default) suits app UIs, slash suits repository and workspace paths, and dot is a quieter option for marketing pages and metadata.
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
export default function BreadcrumbsSeparators () {
return (
< div className = "flex flex-col gap-4" >
{([ "chevron" , "slash" , "dot" ] as const ). map (( separator ) => (
< Breadcrumbs key = {separator} separator = {separator}>
< Breadcrumb href = "#" >Docs</ Breadcrumb >
< Breadcrumb href = "#" >Components</ Breadcrumb >
< Breadcrumb >Breadcrumbs</ Breadcrumb >
</ Breadcrumbs >
))}
</ div >
);
}
md (14px, default) above page titles, sm (12px) in dense headers, toolbars and cards.
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
export default function BreadcrumbsSizes () {
return (
< div className = "flex flex-col gap-4" >
{([ "sm" , "md" ] as const ). map (( size ) => (
< Breadcrumbs key = {size} size = {size}>
< Breadcrumb href = "#" >Store</ Breadcrumb >
< Breadcrumb href = "#" >Audio</ Breadcrumb >
< Breadcrumb href = "#" >Headphones</ Breadcrumb >
< Breadcrumb >Studio Pro Wireless</ Breadcrumb >
</ Breadcrumbs >
))}
</ div >
);
}
Pass icon to show an icon before the label. Icons inside the link are sized to 14px.
Home DocumentsQ3 report.pdf
import { FolderIcon, HomeIcon } from "lucide-react" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
export default function BreadcrumbsIcons () {
return (
< Breadcrumbs size = "sm" >
< Breadcrumb href = "#" icon = {< HomeIcon />}>
Home
</ Breadcrumb >
< Breadcrumb href = "#" icon = {< FolderIcon />}>
Documents
</ Breadcrumb >
< Breadcrumb >Q3 report.pdf</ Breadcrumb >
</ Breadcrumbs >
);
}
Keep the first level compact with an icon, and put the label in an sr-only span so the link still has an accessible name.
Home Help Center Billing Update a payment method
import { HomeIcon } from "lucide-react" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
export default function BreadcrumbsIconHome () {
return (
< Breadcrumbs >
< Breadcrumb href = "#" icon = {< HomeIcon />}>
< span className = "sr-only" >Home</ span >
</ Breadcrumb >
< Breadcrumb href = "#" >Help Center</ Breadcrumb >
< Breadcrumb href = "#" >Billing</ Breadcrumb >
< Breadcrumb >Update a payment method</ Breadcrumb >
</ Breadcrumbs >
);
}
Wrap a label in a <span> with max-w-* and truncate to cap its width, and repeat the full text in title so it's available on hover. The full text stays in the accessibility tree.
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
const crumbs = [
"Engineering" ,
"Platform Infrastructure and Developer Experience" ,
"Quarterly planning" ,
"Q3 2026 roadmap: service mesh migration and cost review" ,
];
export default function BreadcrumbsTruncation () {
return (
< Breadcrumbs className = "max-w-md" >
{crumbs. map (( label , i ) => (
< Breadcrumb key = {label} href = {i < crumbs. length - 1 ? "#" : undefined }>
< span className = "max-w-32 truncate" title = {label}>
{label}
</ span >
</ Breadcrumb >
))}
</ Breadcrumbs >
);
}
Pass items and a render function instead of static children. Each item needs a unique id.
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
const path = [
{ id: "org" , label: "Acme Inc" , href: "#" },
{ id: "team" , label: "Growth" , href: "#" },
{ id: "project" , label: "Onboarding revamp" , href: "#" },
{ id: "doc" , label: "Research notes" },
];
export default function BreadcrumbsDynamic () {
return (
< Breadcrumbs items = {path}>
{( item ) => (
< Breadcrumb id = {item.id} href = {item.href}>
{item.label}
</ Breadcrumb >
)}
</ Breadcrumbs >
);
}
Without href, items render as link-role <span>s and pressing one calls onAction on Breadcrumbs with the item's id. Use this for in-app navigation that isn't URL based, like a folder picker.
import { FileTextIcon, FolderIcon } from "lucide-react" ;
import { useState } from "react" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
import { Button } from "@/components/ui/button" ;
const folders : Record < string , string []> = {
Drive: [ "Design" , "Finance" ],
Design: [ "Brand" , "Product" ],
Finance: [ "Invoices" ],
Brand: [],
Product: [],
Invoices: [],
};
export default function BreadcrumbsOnAction () {
const [ path , setPath ] = useState ([ "Drive" , "Design" , "Brand" ]);
const current = path[path. length - 1 ];
return (
< div className = "grid w-full max-w-sm gap-3" >
< Breadcrumbs
items = {path. map (( name ) => ({ id: name }))}
onAction = {( key ) =>
setPath (path. slice ( 0 , path. indexOf ( String (key)) + 1 ))
}
>
{( item ) => < Breadcrumb id = {item.id}>{item.id}</ Breadcrumb >}
</ Breadcrumbs >
< ul className = "grid gap-1 rounded-lg border bg-card p-1 text-sm" >
{folders[current]. map (( name ) => (
< li key = {name}>
< Button
variant = "ghost"
className = "w-full justify-start font-normal"
onPress = {() => setPath ([ ... path, name])}
>
< FolderIcon className = "text-muted-foreground" /> {name}
</ Button >
</ li >
))}
{folders[current]. length === 0 && (
< li className = "flex h-8 items-center gap-2 px-3 text-muted-foreground" >
< FileTextIcon className = "size-4" /> guidelines.pdf
</ li >
)}
</ ul >
</ div >
);
}
isDisabled on Breadcrumbs makes every item non-interactive. The links are dimmed to 50% opacity with a not-allowed cursor; the current item keeps its normal style.
import { useState } from "react" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
import { Switch } from "@/components/ui/switch" ;
export default function BreadcrumbsDisabled () {
const [ isDisabled , setDisabled ] = useState ( true );
return (
< div className = "grid justify-items-start gap-4" >
< Breadcrumbs isDisabled = {isDisabled}>
< Breadcrumb href = "#" >Home</ Breadcrumb >
< Breadcrumb href = "#" >Projects</ Breadcrumb >
< Breadcrumb >Settings</ Breadcrumb >
</ Breadcrumbs >
< Switch isSelected = {isDisabled} onChange = {setDisabled}>
Disabled
</ Switch >
</ div >
);
}
Breadcrumb links are React Aria Links, so they go through your router when the app is wrapped in React Aria's RouterProvider. With the Next.js App Router:
"use client" ;
import { useRouter } from "next/navigation" ;
import { RouterProvider } from "react-aria-components" ;
export function Providers ({ children } : { children : React . ReactNode }) {
const router = useRouter ();
return < RouterProvider navigate = {router.push}>{children}</ RouterProvider >;
}
< Breadcrumbs >
< Breadcrumb href = "/settings" >Settings</ Breadcrumb >
< Breadcrumb href = "/settings/team" >Team</ Breadcrumb >
< Breadcrumb >Roles</ Breadcrumb >
</ Breadcrumbs >
Small breadcrumbs above the page title, with a status Badge and actions on the right.
import { PencilIcon } from "lucide-react" ;
import { Badge } from "@/components/ui/badge" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
import { Button } from "@/components/ui/button" ;
export default function BreadcrumbsRecipePageHeader () {
return (
< header className = "flex w-full max-w-2xl flex-wrap items-end justify-between gap-4 border-b pb-4" >
< div className = "grid gap-2" >
< Breadcrumbs size = "sm" >
< Breadcrumb href = "#" >Products</ Breadcrumb >
< Breadcrumb href = "#" >Headphones</ Breadcrumb >
< Breadcrumb >Studio Pro Wireless</ Breadcrumb >
</ Breadcrumbs >
< div className = "flex items-center gap-2" >
< h1 className = "font-semibold text-xl tracking-tight" >
Studio Pro Wireless
</ h1 >
< Badge variant = "dot" color = "success" >
Active
</ Badge >
</ div >
< p className = "text-muted-foreground text-sm" >
SKU HP-2041 · 318 in stock · Updated 2 hours ago
</ p >
</ div >
< div className = "flex gap-2" >
< Button variant = "outline" >
< PencilIcon /> Edit
</ Button >
< Button >View in store</ Button >
</ div >
</ header >
);
}
For deep hierarchies, keep the first and last levels and move the middle ones into a Menu . The collapsed level is a Breadcrumb with link={false}, which renders its children (here a menu trigger) as-is instead of wrapping them in a link, and still gets a separator.
Wiki Incidents Database failover
import { MoreHorizontalIcon } from "lucide-react" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
import { Button } from "@/components/ui/button" ;
import { MenuContent, MenuItem, MenuTrigger } from "@/components/ui/menu" ;
const hidden = [ "Engineering" , "Platform" , "Runbooks" ];
export default function BreadcrumbsRecipeCollapsed () {
return (
< Breadcrumbs >
< Breadcrumb href = "#" >Wiki</ Breadcrumb >
{ /* link={false} renders the menu trigger as-is, keeping the separator. */ }
< Breadcrumb link = { false }>
< MenuTrigger >
< Button
variant = "ghost"
size = "icon-xs"
aria-label = { `Show ${ hidden . length } more` }
className = "text-muted-foreground"
>
< MoreHorizontalIcon />
</ Button >
< MenuContent placement = "bottom start" >
{hidden. map (( label ) => (
< MenuItem key = {label} href = "#" >
{label}
</ MenuItem >
))}
</ MenuContent >
</ MenuTrigger >
</ Breadcrumb >
< Breadcrumb href = "#" >Incidents</ Breadcrumb >
< Breadcrumb >Database failover</ Breadcrumb >
</ Breadcrumbs >
);
}
A workspace path in an app's top bar: an Avatar as the first icon, a plan badge inside a crumb, and a slash separator. The breadcrumbs are wrapped in a <nav> landmark.
import { ChevronsUpDownIcon, GitBranchIcon } from "lucide-react" ;
import { Avatar } from "@/components/ui/avatar" ;
import { Badge } from "@/components/ui/badge" ;
import { Breadcrumb, Breadcrumbs } from "@/components/ui/breadcrumbs" ;
import { Button } from "@/components/ui/button" ;
export default function BreadcrumbsRecipeAppHeader () {
return (
< div className = "flex h-12 w-full max-w-2xl items-center justify-between gap-3 rounded-lg border bg-card px-3" >
< nav aria-label = "Location" className = "min-w-0" >
< Breadcrumbs separator = "slash" >
< Breadcrumb
href = "#"
icon = {
< Avatar
size = "xs"
shape = "square"
colorful
fallback = "A"
alt = "Acme"
/>
}
>
Acme
</ Breadcrumb >
< Breadcrumb href = "#" >
acme-web
< Badge size = "sm" variant = "outline" >
Pro
</ Badge >
</ Breadcrumb >
< Breadcrumb icon = {< GitBranchIcon />}>feat/checkout-v2</ Breadcrumb >
</ Breadcrumbs >
</ nav >
< Button variant = "ghost" size = "icon-sm" aria-label = "Switch project" >
< ChevronsUpDownIcon />
</ Button >
</ div >
);
}
Renders an ordered list (<ol>) labelled "Breadcrumbs" by default. Pass aria-label to change it, and wrap the list in a <nav> element when it's the page's breadcrumb trail so it's exposed as a navigation landmark.
The last item has aria-current="page" and isn't focusable or interactive.
Separators are aria-hidden, so screen readers announce only the labels.
Items with an href are native links: middle-click, "open in new tab" and the browser's link preview work.
Give icon-only items a text label with sr-only. aria-label on Breadcrumb is applied to the <li>, not the link.
Key Action Tab / Shift +Tab Moves between breadcrumb links (the current item is skipped) Enter Follows the focused link, or calls onAction
On each Breadcrumb (<li>) and its link:
Attribute Present when data-currentIt's the last item (the current page) data-disabledIt's the current item, or isDisabled is set on Breadcrumbs
On the link only:
Attribute Present when data-hovered / data-pressedHovered / being pressed data-focused / data-focus-visibleFocused / focused with the keyboard
The current item's link is font-medium text-foreground; the others are text-muted-foreground and turn text-foreground on hover. Disabled links (other than the current item) get opacity-50 and cursor-not-allowed.
data-slotElement breadcrumbs<ol> rootbreadcrumbEach <li>
className on Breadcrumbs styles the list (a wrapping flex row with gap-1.5). It must be a string.
className on Breadcrumb styles the <li>, and accepts a function of { isCurrent, isDisabled }.
To restyle the links, target them from the list, e.g. className="[&_a]:underline".
With link={false}, the children aren't wrapped or styled, and isDisabled on Breadcrumbs doesn't reach them. Disable a menu trigger inside yourself.
Also accepts every prop of React Aria's Breadcrumbs .
Link — the link used inside each breadcrumb.
Sidebar — primary navigation, often paired with breadcrumbs in the content header.
Menu — for collapsed levels in deep paths.
Tabs — switching between sibling views.