Breadcrumb
Where the current page sits in the hierarchy, and a way back up it. Four rules and a generated separator — the smallest component in Deck, and one where the markup matters more than the styling.
When to use it
On a site with real depth, where a page has ancestors someone might want to go back to: documentation, a catalogue, a file browser. The trail answers "where am I" and "how do I go up one".
On a flat site it is decoration — a breadcrumb with one ancestor tells the reader nothing they did not know from the nav bar.
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="#">Docs</a></li>
<li><a href="#">Components</a></li>
<li aria-current="page">Breadcrumb</li>
</ol>
</nav>
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="#">Docs</a></li>
<li><a href="#">Components</a></li>
<li aria-current="page">Breadcrumb</li>
</ol>
</nav>
<ol>, because the order is the hierarchy. An
<ol> with a class already has its markers and padding removed by
src/02-reset.css — the ul[class], ol[class] rule — so no list
reset is needed alongside .breadcrumb.
The separator is generated content
The slash between items is not in your markup. It is a pseudo-element on every item after the first:
.breadcrumb li + li::before {
content: "/";
color: var(--text-faint);
margin-inline-end: var(--space-2);
}
That is the right way to do it — a separator is presentation, and putting it in the HTML means it gets copied, translated and read aloud as content. Two things follow from generated content, though, and both are worth knowing:
-
Some screen readers announce it. CSS
contentis exposed by most engines, so the trail may be read as "Docs slash Components slash Breadcrumb". That is tolerable — it is short and it does convey separation — but it is not silent, and you cannotaria-hiddena pseudo-element. - It does not survive a copy-paste in most browsers, which is usually a benefit: the reader gets the words rather than the punctuation.
margin-inline-end puts the gap after the slash, and
.breadcrumb's own gap supplies the space before it. Both are
logical, so the spacing holds up in either direction.
The last item
The current page belongs in the trail — it is the "you are here" — but it should not be
a link. [aria-current] gives it full-strength text and a heavier weight,
and the bare attribute selector means any value matches.
<div class="stack stack-3">
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="#">Catalogue</a></li>
<li><a href="#">Lighting</a></li>
<li><a href="#">Desk lamps</a></li>
<li aria-current="page">Anglepoise 1227</li>
</ol>
</nav>
<nav aria-label="Breadcrumb, truncated">
<ol class="breadcrumb">
<li><a href="#">Catalogue</a></li>
<li><span aria-hidden="true">…</span></li>
<li><a href="#">Desk lamps</a></li>
<li aria-current="page">Anglepoise 1227</li>
</ol>
</nav>
</div>
<div class="stack stack-3">
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="#">Catalogue</a></li>
<li><a href="#">Lighting</a></li>
<li><a href="#">Desk lamps</a></li>
<li aria-current="page">Anglepoise 1227</li>
</ol>
</nav>
<nav aria-label="Breadcrumb, truncated">
<ol class="breadcrumb">
<li><a href="#">Catalogue</a></li>
<li><span aria-hidden="true">…</span></li>
<li><a href="#">Desk lamps</a></li>
<li aria-current="page">Anglepoise 1227</li>
</ol>
</nav>
</div>
The container is flex-wrap: wrap, so a long trail wraps to a second line
rather than overflowing. On a narrow screen that is usually preferable to truncation —
but if the trail is deep, collapsing the middle as above keeps it to one line.
Classes
.breadcrumb is a single class with no members — everything else is
descendant and attribute selectors — so there is no completeness check on this page.
| Class | Layer | What it does | Source |
|---|---|---|---|
.breadcrumb |
deck.components |
Sets display, flex-wrap, align-items, gap, and 1 more. | 07-components.css:473 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--space-2 |
.5rem |
|
--text-sm |
.8125rem |
|
--text |
light-dark(var(--ink-900), var(--ink-100)) |
|
--text-muted |
light-dark(var(--ink-600), var(--ink-400)) |
|
--text-faint |
light-dark(var(--ink-500), var(--ink-500)) |
|
--brand |
light-dark(var(--brand-600), var(--brand-400)) |
Accessibility
-
Wrap it in
<nav aria-label="Breadcrumb">..breadcrumbis a class and confers no role. The label is what distinguishes it from the other navigation regions on the page. - Use an ordered list. The hierarchy is an order. It also gives a screen reader a count, so the reader knows how deep they are before hearing every level.
-
Mark the current page and do not link it.
aria-current="page"on the last<li>. A link to the page you are on is a tab stop that goes nowhere. - The separator may be announced. See above — generated content is exposed by most engines. It cannot be hidden, and it is short enough not to be a problem, but it is worth knowing before you choose a more elaborate separator character.
-
Hide a truncation ellipsis.
aria-hidden="true"on the "…", and if the hidden levels are reachable some other way, make sure that way is obvious. - Do not use it as the only way back. It is a supplement to the main navigation, not a replacement. A reader who arrives from a search engine may have no other route upward, and a trail of two items is not much of one.
Right to left
The row is flex and every offset is logical, so the trail runs from the right and the
separator's spacing follows. No rule in src/19-logical.css is needed.
The slash is worth a thought: / is a bidirectionally neutral character, so
it does not mirror and does not need to — it reads as a separator in either direction.
A chevron would be a different story, and would need
an icon that flips rather than a text glyph.
<nav dir="rtl" aria-label="مسار التنقل">
<ol class="breadcrumb">
<li><a href="#">المستندات</a></li>
<li><a href="#">المكوّنات</a></li>
<li aria-current="page">مسار التنقل</li>
</ol>
</nav>
<nav dir="rtl" aria-label="مسار التنقل">
<ol class="breadcrumb">
<li><a href="#">المستندات</a></li>
<li><a href="#">المكوّنات</a></li>
<li aria-current="page">مسار التنقل</li>
</ol>
</nav>
Reduced motion
Nothing here animates. The hover colour change on a link is instant, and there are no
transitions, transforms or keyframes in the component — so
prefers-reduced-motion has nothing to act on.
Printing
.breadcrumb is not in src/99-print.css, so it prints — unlike
pagination and the
sidebar, which are both removed.
That is arguably right: the trail is the document's position in a hierarchy, which is
information about the page rather than a control. But note that the print stylesheet
appends the target URL after every link whose href starts with
http, so a printed breadcrumb of absolute links becomes a row of items each
trailing a full address. Relative and in-page links are already exempt. If your trail
uses absolute URLs, put .no-print-url on each
<a> — the rule is .no-print-url::after, so it has to be
on the link itself, not the list — or suppress them all at once:
@media print {
/* keep the words, drop the addresses */
.breadcrumb a::after { content: none; }
}
Overriding it
@layer app.components {
/* A chevron instead of a slash */
.breadcrumb li + li::before { content: "›"; }
/* Truncate long titles rather than wrapping the trail */
.breadcrumb li {
max-inline-size: 14ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.breadcrumb li:last-child { max-inline-size: none; }
}
If you truncate, leave the last item alone — the current page is the one the reader most needs to read in full.
When not to use it
- Not on a flat site. One ancestor is not a trail, and it competes with the nav bar for the same job.
- Not as a history of where the reader has been. A breadcrumb shows the site's hierarchy, not the path taken through it. Using it for the latter surprises people, because the back button already does that and does it correctly.
- Not as a substitute for a back button in a flow. In a checkout or a wizard, "back" means the previous step, which is a stepper concern.
- Not with the current page as a link. See The last item.
- Not for more than about five levels. Past that the trail is longer than the content it labels. Collapse the middle.