Breadcrumbs
Tells the user where they are in a nested structure and gives them one click back up.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Breadcrumbs is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Breadcrumbs } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Tells the user where they are in a nested structure and gives them one click back up. Use it for hierarchies at least three levels deep.
Rules
- The last crumb is the current page. It renders as text with
aria-current="page", never as a link to itself. - Breadcrumbs reflect hierarchy, not history. Do not build them from the back stack; that is what the browser button is for.
- Keep labels short. Truncate long titles rather than wrapping the trail onto two lines.
- Two levels does not need a trail. Use a single back link instead.
Tradeoffs
Deep trails eat a row of vertical space on every page to serve a minority of navigation events. On mobile, consider collapsing the middle crumbs to an ellipsis.
Props
The typed contract, from Breadcrumbs.d.ts.
import * as React from "react";
export interface Crumb {
label: React.ReactNode;
href?: string;
onClick?: (e: React.MouseEvent) => void;
}
/** Shows where the current page sits in the hierarchy. */
export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
/** Root first, current page last. The last item renders as text, not a link. */
items: Crumb[];
/** @default "Breadcrumb" */
label?: string;
/** @default "/" */
separator?: React.ReactNode;
}
export declare function Breadcrumbs(props: BreadcrumbsProps): React.JSX.Element;
Tokens it reads
Every custom property Breadcrumbs.jsx resolves, read from the source: 6 semantic, 1 primitive. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
- primitive Raw values. A component reading one directly is a deliberate exception.
| Token | Tier | Declared as |
|---|---|---|
--dt-space-inline-xs | semantic | var(--dt-dim-2) |
--dt-text-body-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-sm-size | semantic | var(--dt-font-size-sm) |
--dt-text-primary | semantic | var(--dt-color-neutral-900) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-text-tertiary | semantic | var(--dt-color-neutral-500) |
--dt-font-weight-medium | primitive | 500 |
Source
React only, no dependencies, every value a token.
import React from "react";
export function Breadcrumbs({ items = [], label = "Breadcrumb", separator = "/", style, ...rest }) {
return (
<nav aria-label={label} style={style} {...rest}>
<ol style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--dt-space-inline-xs)" }}>
{items.map((it, i) => {
const last = i === items.length - 1;
return (
<li key={it.href || i} style={{ display: "flex", alignItems: "center", gap: "var(--dt-space-inline-xs)", minWidth: 0 }}>
{last ? (
<span aria-current="page" style={{
fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)",
color: "var(--dt-text-primary)", fontWeight: "var(--dt-font-weight-medium)",
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
}}>{it.label}</span>
) : (
<a href={it.href} onClick={it.onClick} style={{
fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)",
color: "var(--dt-text-secondary)", textDecoration: "none",
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
}}>{it.label}</a>
)}
{!last && <span aria-hidden="true" style={{ color: "var(--dt-text-tertiary)", fontSize: "var(--dt-text-body-sm-size)" }}>{separator}</span>}
</li>
);
})}
</ol>
</nav>
);
}
In context
Breadcrumbs also appears on the navigation reference cards, alongside the rest of its family.