Breadcrumb.
A path with context. Resolve route patterns, name dynamic resources, and keep long journeys within reach.
Follow the path
One trail. Your application's rules.
Pick a scenario and change the data state, unknown-segment policy, and visible slots. The breadcrumb in this site's top bar is the same component resolving the real pathname.
/workspace/projects/atlas/tasks/task-42/activityLinks update this local path only. Dynamic names come from demo data; no request is made.
Resolved items
[
{
"id": "/",
"label": "Home",
"href": "/",
"current": false
},
{
"id": "/workspace",
"label": "Workspace",
"href": "/workspace",
"current": false
},
{
"id": "/workspace/projects",
"label": "Projects",
"href": "/workspace/projects",
"current": false
},
{
"id": "project",
"label": "Atlas",
"href": "/workspace/projects/atlas",
"current": false
},
{
"id": "/workspace/projects/:projectId/tasks",
"label": "Tasks",
"href": "/workspace/projects/atlas/tasks",
"current": false
},
{
"id": "/workspace/projects/:projectId/tasks/:taskId",
"label": "Review the component library",
"href": "/workspace/projects/atlas/tasks/task-42",
"current": false
},
{
"id": "/workspace/projects/:projectId/tasks/:taskId/activity",
"label": "Activity",
"href": "/workspace/projects/atlas/tasks/task-42/activity",
"current": true
}
]Route metadata
Names belong to your data.
Read pathname from your router and use its Link in renderLink. Label functions are synchronous; loading and missing-data labels are explicit. API calls, caching, permissions, and translations stay in your application.
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AppBreadcrumb, type BreadcrumbRoute } from "@/components/ui/app-breadcrumb";
type RouteData = { project: { name: string } };
const routes: BreadcrumbRoute<RouteData>[] = [
{ path: "/", label: "Home" },
{ path: "/projects", label: "Projects" },
{
path: "/projects/:projectId",
label: ({ data }) => data?.project.name,
fallbackLabel: "Project unavailable",
},
];
export function Header({ project, isLoading }) {
const pathname = usePathname();
return (
<AppBreadcrumb
pathname={pathname}
routes={routes}
data={{ project }}
loading={isLoading}
maxVisibleItems={5}
compactMaxVisibleItems={3}
renderLink={({ href, children }) => <Link href={href}>{children}</Link>}
/>
);
}// A hierarchy that differs from URL nesting.
{ id: "project", path: "/projects/:projectId", label: projectLabel }
{ path: "/reports/:projectId/weekly", parentId: "project", label: "Weekly report" }
// Skip a level but keep its ancestors.
{ path: "/workspace", label: "Workspace", hidden: true }
// Start a new hierarchy; no URL ancestors are rendered.
{ path: "/admin", label: "Admin", parentId: null }
// Resolve without rendering, for a custom view.
const items = resolveBreadcrumbs({ pathname, routes, data });Static segments take priority over :parameters, then terminal *catchAll patterns. Invalid patterns, duplicate IDs, cyclic parents, and unsafe links fail with configuration errors.
Layout + page
Let the page name its own trail.
A layout renders the provider and the breadcrumb. A nested page publishes labels for the resources it loaded. Scope includes route parameters, so an inactive page can never change the current trail.
The layout and page publishers are siblings. Unmounting the active page restores the provider fallback.
// Layout: scope identifies the active route instance.
<BreadcrumbProvider scope={routeKey} pathname={pathname} routes={routes}>
<header><ContextBreadcrumb /></header>
{children}
</BreadcrumbProvider>
// A deeply nested page publishes a full trail, or only the labels it knows.
usePageBreadcrumbs({ scope: routeKey, routeLabels: { project: project.name } });
// Or: usePageBreadcrumbs({ scope: routeKey, items });Explicit when needed
Manual items
When there is no router, or the trail is not derived from the URL, pass items directly. The last item is current.
Visible-item limits include the overflow trigger. The first and current items remain visible; intermediate links move into a keyboard-accessible menu. Long labels truncate visually while their full text remains in the markup and title.
Reference
Props
AppBreadcrumb accepts either a resolved mode (pathname and routes) or a manual mode (items). BreadcrumbProvider takes the same props plus scope.
| Prop | Type | Default |
|---|---|---|
pathnameThe current location from your router. | string | — |
routesRoute patterns to resolve against. | BreadcrumbRoute<TData>[] | — |
dataLoaded data passed to label and href resolvers. | TData | — |
loadingMarks function labels pending. Static labels stay visible. | boolean | — |
unknownPolicy for segments with no matching route. | "format" | "omit" | "error" | (context) => ReactNode | "format" |
itemsManual trail. Mutually exclusive with pathname and routes. | BreadcrumbEntry[] | — |
maxVisibleItemsVisible slots including the overflow trigger. Minimum 3. | number | 5 |
compactMaxVisibleItemsVisible slots below 768px. | number | 3 |
separatorRendered between items. | ReactNode | — |
renderLinkUse your router's Link. | ({ href, children, item }) => ReactElement | — |
labelsAccessible names and fallback strings. | Partial<BreadcrumbLabels> | — |
| Prop | Type | Default |
|---|---|---|
pathPattern with static segments, :params, and a terminal *catchAll. | string | — |
labelStatic text or a synchronous resolver receiving params, data, pathname, and path. | ReactNode | (context) => ReactNode | — |
idReferenced by parentId to build custom hierarchies. | string | path |
parentIdundefined follows URL prefixes; null starts a new hierarchy. | string | null | — |
hiddenSkip this level but keep its ancestors. | boolean | false |
hrefOverride the navigable URL. null renders a non-navigable ancestor. | string | null | (context) => string | null | — |
loadingLabelShown while loading applies to a function label. | ReactNode | — |
fallbackLabelShown when a function label returns nothing. | ReactNode | — |