role navigation
Breadcrumb draft v0.1.0
Shows where the current page sits in a hierarchy, with a link to each level above it.
A breadcrumb lists the pages above the current one, from the top of the hierarchy down, each as a link, and ends with the current page, which is not a link. It tells people where they are and lets them go up one or several levels in one activation. It is a navigation landmark of its own, separate from the main navigation.
Web implementation
Root .ds-breadcrumb on a <nav>. Properties map to attributes: size → data-sizelabel → aria-label
packages/components/src/components/breadcrumb/breadcrumb.css
/*
* Breadcrumb — vanilla CSS reference implementation.
* Spec: packages/components/specs/components/breadcrumb.spec.json
* Tokens: packages/tokens/dist/css/components/breadcrumb.css (generated from packages/tokens/src/components/breadcrumb.tokens.json)
*
* Rules:
* - Only --breadcrumb-* custom properties may appear here. No raw values, no semantic tokens.
* - The root is a nav landmark; items are an ordered list of links, separated by a chevron; the
* last item is the current page.
* - Size is a data attribute on the root; link states are native pseudo-classes, forced with
* [data-state] on the root (every item) or on one item.
*/
.ds-breadcrumb {
--_font-family: var(--breadcrumb-link-typography-medium-font-family);
--_font-size: var(--breadcrumb-link-typography-medium-font-size);
--_font-weight: var(--breadcrumb-link-typography-medium-font-weight);
--_line-height: var(--breadcrumb-link-typography-medium-line-height);
--_letter-spacing: var(--breadcrumb-link-typography-medium-letter-spacing);
--_separator-size: var(--breadcrumb-separator-size-medium);
box-sizing: border-box;
min-inline-size: 0;
}
.ds-breadcrumb[data-size="small"] {
--_font-family: var(--breadcrumb-link-typography-small-font-family);
--_font-size: var(--breadcrumb-link-typography-small-font-size);
--_font-weight: var(--breadcrumb-link-typography-small-font-weight);
--_line-height: var(--breadcrumb-link-typography-small-line-height);
--_letter-spacing: var(--breadcrumb-link-typography-small-letter-spacing);
--_separator-size: var(--breadcrumb-separator-size-small);
}
.ds-breadcrumb__list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--breadcrumb-container-gap);
margin: 0;
padding: 0;
list-style: none;
}
.ds-breadcrumb__item {
--_color: var(--breadcrumb-link-color-default);
display: inline-flex;
align-items: center;
gap: var(--breadcrumb-container-gap);
}
.ds-breadcrumb__link {
display: inline-flex;
align-items: center;
min-height: var(--breadcrumb-link-min-height);
border-radius: var(--breadcrumb-link-border-radius);
color: var(--_color);
font-family: var(--_font-family);
font-size: var(--_font-size);
font-weight: var(--_font-weight);
line-height: var(--_line-height);
letter-spacing: var(--_letter-spacing);
text-decoration: underline;
text-underline-offset: calc(var(--_font-size) * 0.15);
transition: color var(--breadcrumb-link-transition-duration) var(--breadcrumb-link-transition-easing);
}
.ds-breadcrumb__separator {
display: inline-block;
flex: none;
inline-size: var(--_separator-size);
block-size: var(--_separator-size);
overflow: hidden;
color: var(--breadcrumb-separator-color);
font-family: var(--breadcrumb-separator-font-family);
font-size: var(--_separator-size);
font-weight: var(--breadcrumb-separator-font-weight);
font-style: normal;
font-variation-settings:
"FILL" var(--breadcrumb-separator-fill),
"GRAD" var(--breadcrumb-separator-grade),
"opsz" var(--breadcrumb-separator-optical-size);
font-feature-settings: "liga";
line-height: var(--_separator-size);
letter-spacing: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
/* ---- States -------------------------------------------------------------- */
:where(.ds-breadcrumb[data-state="hover"], .ds-breadcrumb__item[data-state="hover"]) .ds-breadcrumb__link,
.ds-breadcrumb__link:hover {
--_color: var(--breadcrumb-link-color-hover);
}
/* The current page: no link, no underline, stronger color. */
:where(.ds-breadcrumb[data-state="current"], .ds-breadcrumb__item[data-state="current"]) .ds-breadcrumb__link,
.ds-breadcrumb__link[aria-current] {
--_color: var(--breadcrumb-link-color-current);
text-decoration: none;
}
.ds-breadcrumb__link:focus {
outline: none;
}
:where(.ds-breadcrumb[data-state="focus-visible"], .ds-breadcrumb__item[data-state="focus-visible"]) .ds-breadcrumb__link,
.ds-breadcrumb__link:focus-visible {
outline: var(--breadcrumb-link-outline-width-focus-visible) solid var(--breadcrumb-link-outline-color-focus-visible);
outline-offset: var(--breadcrumb-link-outline-offset-focus-visible);
}
@media (prefers-reduced-motion: reduce) {
.ds-breadcrumb__link {
transition: none;
}
}
packages/components/src/components/breadcrumb/breadcrumb.template.mjs
/**
* Breadcrumb — HTML template for the web implementation.
* Pure function of the spec properties (packages/components/specs/components/breadcrumb.spec.json) -> HTML string.
* A nav landmark labelled "Breadcrumb" holding an ordered list of links; the last item is the
* current page, marked with aria-current="page" and not linked. The separators are decorative.
*
* Extra sandbox-only props: `state` on the root forces a state on every item; an entry's `state`
* forces it on that item.
*/
import { escapeHtml, escapeAttr, renderIcon } from '../../lib/template-utils.mjs';
export const meta = { id: 'breadcrumb', selector: '.ds-breadcrumb' };
export function render(props = {}) {
const { items = [{ label: 'Home', href: '#' }, { label: 'Current page' }], label = 'Breadcrumb', size = 'medium', state = null } = props;
const rootAttrs = ['class="ds-breadcrumb"', `aria-label="${escapeAttr(label)}"`, `data-size="${escapeAttr(size)}"`];
if (state && state !== 'default') rootAttrs.push(`data-state="${escapeAttr(state)}"`);
const last = items.length - 1;
const list = items.map((entry, i) => {
const { label: text = `Page ${i + 1}`, href = null, state: itemState = null } = entry;
const current = i === last;
const itemAttrs = ['class="ds-breadcrumb__item"', 'data-part="item"'];
if (itemState && itemState !== 'default') itemAttrs.push(`data-state="${escapeAttr(itemState)}"`);
const link = current
? `<span class="ds-breadcrumb__link" data-part="link" aria-current="page">${escapeHtml(text)}</span>`
: `<a class="ds-breadcrumb__link" data-part="link" href="${escapeAttr(href ?? '#')}">${escapeHtml(text)}</a>`;
const separator = current ? '' : `<span class="ds-breadcrumb__separator" data-part="separator" aria-hidden="true">${renderIcon('chevron_right')}</span>`;
return `<li ${itemAttrs.join(' ')}>${link}${separator}</li>`;
});
return `<nav ${rootAttrs.join(' ')}><ol class="ds-breadcrumb__list" data-part="list">${list.join('')}</ol></nav>`;
}
No other platform implementation recorded yet. Platform token outputs live under Platforms.