Components
Breadcrumb
One line above a subpage title saying where the page sits. A list of links, the page
you are on as a <span>, and a separator that is drawn rather than
typed — a 1 px hairline on the steep isometric angle. No script, no glyph, no
character to hide from a screen reader.
Default
Three levels, which is what a blog article on this site actually has. The last crumb is the page you are on: not a link, and set in full ink so the one thing that has to be distinguishable — which crumb is not a target — is distinguishable without hovering anything.
Two levels and four
Two is the floor at which the component says anything at all, and four is the ceiling. Past four the trail is telling you the information architecture is too deep, and the answer is the architecture rather than an ellipsis.
The fold
Below 30 rem of container the trail keeps the parent and the current page and removes everything above them. It is a container query, not a media query, so the component folds on its own width — the same contract Pagination folds under. The box below is 22 rem wide at any viewport, so this is the folded state whatever you are reading it on.
The current crumb is also the only item allowed to shrink, so a long title truncates with an ellipsis rather than pushing the trail sideways. An ancestor never truncates: a link whose destination you can no longer read is worse than a link that is not there.
The separator
Every other system reaches for a character — /, ›,
» — set in a ::before. Three things are wrong with that here.
- It is a glyph from the body face standing in a row of mono labels: the only mark on the line drawn by somebody else.
- Its angle is whatever the typeface felt like. A solidus leans about 70°, which is none of the four angles the brand sanctions.
-
CSS
contentwith text in it is announced by some screen readers, so the trail reads “News slash Analyse slash”. Every system that uses one then has to work out how to hide it again.
content: "" on a 1 px box rotated to a brand angle has none of those
problems. There is no text, so there is nothing to announce and nothing to suppress.
It is a hairline, which is what this system draws everything with. And it lands on
--angle-a, 63.43° — the steep isometric, the same line the lattice runs.
The rotation is written as --angle-b because the box starts vertical and
the two angles are complements: 90 − 26.57 = 63.43. Move --angle-b and
every separator on the site follows it.
Markup
<nav class="cf-breadcrumb" aria-label="Sie sind hier"> <ol class="cf-breadcrumb__list" role="list"> <li class="cf-breadcrumb__item"><a class="cf-breadcrumb__link" href="/">Home</a></li> <li class="cf-breadcrumb__item"><a class="cf-breadcrumb__link" href="/news">News</a></li> <!-- the current page is a span, never a link --> <li class="cf-breadcrumb__item"> <span class="cf-breadcrumb__current" aria-current="page">Digitale Zwillinge im Energiesektor</span> </li> </ol> </nav>
It goes inside .cf-page-header__body, immediately above the title, and it
brings its own bottom margin — the component owns that distance the way
.cf-pagination owns the air above itself, because it belongs to the shape
and every page wants the same one. Nothing else on the page sets it.
Anatomy
| Part | Value |
|---|---|
| Row | Geist Mono, 11 px, uppercase, --tracking-label, one line, never wraps |
| Crumb at rest | --text-secondary, no underline, no contour |
| Crumb on hover or focus | --text-primary plus an underline at 0.25em offset, 120 ms |
| Current crumb | --text-primary, a <span>, truncates with an ellipsis |
| Separator | a 12 px border-inline-start in currentColor, rotated to --angle-a (63.43°) |
| Separator margins | --space-3 (12 px) each side — about 9 px of visible air after the lean |
| Target | 28 px tall (--space-2 above and below the 11 px line), ~29 px apart |
| Air below the trail | --space-4 (16 px), the component's own margin |
| Fold | 30 rem of container — parent and current page kept, everything above removed |
| Maximum depth | four crumbs, counting the current page |
Where it goes
-
On a page that has a parent worth naming. Today that is the blog
article —
Home › News › Artikel. It ships on patterns/blog-artikel.html and nowhere else yet. -
Not on a top-level page. News, Kontakt, Über uns and Karriere hang
directly off the home page, and
Home › Kontakttells a reader something the nav bar already has highlighted. USWDS names the same exclusions: landing pages, home pages, and simple sites. - Not on 404. There is no hierarchy above an address that does not exist, and a trail that claims one is a guess.
- Not for a step-by-step process. A trail says where you are in a structure, not how far through a sequence — that is Arrival & Progress.
-
A filter is not a level.
news-thema-energie.htmlis a filtered view of the listing, not a page above the article, so it never appears in the trail. The trail is the hierarchy, not the route the reader took to get here.
BreadcrumbList in JSON-LD does the same
job with no markup in the trail at all — one itemListElement per crumb,
in order. It is worth having, and it is deliberately not hard-coded into the pattern
page: a machine-readable trail that
drifts from the visible one is worse than none, so it has to be generated from the
same data the crumbs are. Whatever renders the trail renders the JSON-LD.
Accessibility
-
A
<nav>landmark with anaria-label. The label is Sie sind hier, the German phrase for exactly this trail — not “Navigation”, which the role already says, and not “Breadcrumb”, which names the mechanism rather than what it tells you. -
An
<ol>, because the order is the meaning.role="list"is on it becauselist-style: noneremoves the role in Safari. -
aria-current="page"on the last crumb, and that crumb is a<span>. The APG allows either a link or plain text there; this system already settled it on Pagination — a link to where you already are is a stop in the tab order that answers nothing. -
The separator is announced by nothing, because it contains nothing.
No
aria-hiddenis needed and none is used. - Targets are 28 px tall and ~29 px apart, past the 24 px floor WCAG 2.2 SC 2.5.8 sets. The inline exception is not being relied on: a nav list is not a sentence.
-
Contrast:
--text-secondaryon the page wash is 5.9:1 and the current crumb is full ink. The separator takescurrentColor, so it is the same 5.9:1 as the row it divides — separators are held to the same AA floor as text. -
The focus ring is the system's, unchanged, and the underline arrives on
:focus-visibleexactly as it does on hover. -
Under forced colours the separator is a border, not a background.
Both draw the identical 1 px line, and the first version used a background — which
forced colours overrides to
Canvas, so in Windows high contrast the trail lost every separator and read as three labels with gaps between them. Border colours are mapped into the forced palette rather than erased. Measured in Chromium's forced-colors emulation, before and after. -
The fold removes crumbs from the accessibility tree, not just from
view —
display: noneis the whole mechanism. That is the intent: on a narrow screen the trail is the parent and the current page, for a pointer and a screen reader alike. What is removed is reachable from the parent.
Rules
Do
- Put it above the page title, inside the page header, on pages two or more levels deep.
- Name the crumbs the way the destination names itself — the nav's word, the listing's word.
- Shorten the current crumb if the title is long. It restates the H1; it does not have to repeat it.
- Keep it to four crumbs. If you need five, the problem is the hierarchy.
- Generate the JSON-LD from the same data as the trail, or ship neither.
Don't
- Don't link the current page, and don't leave it out — it is what carries
aria-current. - Don't put a filter, a search or a tag in the trail. Hierarchy only.
- Don't use a character as the separator, and don't put an icon between crumbs.
- Don't let it wrap to a second line — two rows of labels stop reading as one path.
- Don't add it to a top-level page, to 404, or to a linear process.