Primitives Navigation
Breadcrumb
A navigation landmark with an ordered list of links from the root of the site to the current page, which carries aria-current="page". The separators are drawn in CSS and never announced. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<nav>,<ol>,aria-current="page",:dir(rtl)- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<nav> + <ol> + aria-current="page"
On this page
Example
Why this implementation exists
A breadcrumb is links in a list inside a navigation landmark, which the WAI-ARIA Authoring Practices spell out: a named <nav>, an <ol>, and aria-current="page" on the last item. Nothing in it needs a script.
The one part that goes wrong in markup is the separator: a typed “/” or “>” is read aloud between every level. breadcrumb.css draws the separators as bordered ::before boxes with no text, mirrored under RTL, so the markup holds only the links.
The browser owns
- the navigation landmark and its name
- the list and the number of levels
- the links, their focus and Enter
- the current page state (aria-current)
Svelte Lean owns
- breadcrumb.css: the row, the separators drawn with borders and mirrored under RTL
- the current page’s weight, the link hover and focus ring, the coarse-pointer height
- the contract
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
import '@svelte-lean/styles/breadcrumb.css';Name the landmark with aria-label="Breadcrumb" (or aria-labelledby), list the levels in an <ol> directly inside it, and mark the last item with aria-current="page".
Do not type separators into the markup; the stylesheet draws them.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <nav aria-label="Breadcrumb" data-slean="breadcrumb"> | – | yes | A named navigation landmark. |
| list | <ol> directly in the root | – | yes | One <li> per level, from the site root to the current page. |
| current page | <a href aria-current="page"> or <span aria-current="page"> | – | yes | The last item. |
Runtime profile
Tier 0: the breadcrumb has no behavior module, the Vite plugin maps breadcrumb to no module, and the page ships no Svelte Lean JavaScript for it.
Accessibility contract
- A named
<nav>landmark, told apart from the page’s other navigation by its name. - An
<ol>: screen readers announce the number of levels. aria-current="page"on the current page, a link or a span.- The separators are empty generated boxes: not in the text, not announced.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab/Shift+Tab | anywhere | Moves between the links (native) |
| Enter | focus on a link | Follows it (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<nav>, <ol>, aria-current | Widely available | Not applicable |
rotate | Baseline 2022 | A separator is an unturned corner |
:dir() | Baseline 2023 | The chevrons point to the right under RTL as well |
Without JavaScript
Fully functional: the levels are plain links.
Server rendering
Static markup, rendered with the page from its route.
Before hydration
Nothing is attached; the links work before and after hydration alike.
Styling
breadcrumb.css lays the items out in a wrapping row, draws each separator as a chevron with two borders on li + li::before (turned toward the inline end and mirrored under :dir(rtl)), mutes the ancestors and sets the current page in the text color. Links get the shared focus ring and, on a coarse pointer, the small control height. The rules use child combinators, so a list nested in an item keeps its own styles. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-space-1 | 0.25rem | gap |
--slean-space-2 | 0.5rem | gap |
--slean-icon-chevron-right | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask |
--slean-radius-sm | 0.375rem | border-radius |
--slean-fg | var(--slean-neutral-12) | color |
--slean-font-weight-medium | 500 | font-weight |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-offset | 2px | outline-offset |
--slean-control-height-sm | 2rem | min-block-size |
State selectors the stylesheet targets, all from the platform or ARIA: :dir(rtl), :focus-visible, :hover, [aria-current="page"], [href].
Controlled integration
The application owns the trail. In SvelteKit it can come from the route: one level per path segment, the last one current.
<script lang="ts">
import { page } from '$app/state';
// A trail from the route: one level per path segment, the last one current.
const trail = $derived.by(() => {
const segments = page.url.pathname.split('/').filter(Boolean);
return [
{ href: '/', label: 'Home' },
...segments.map((segment, i) => ({
href: '/' + segments.slice(0, i + 1).join('/'),
label: segment.replaceAll('-', ' ')
}))
];
});
</script>
<nav aria-label="Breadcrumb" data-slean="breadcrumb">
<ol>
{#each trail as level, i (level.href)}
<li>
<a href={level.href} aria-current={i === trail.length - 1 ? 'page' : undefined}>
{level.label}
</a>
</li>
{/each}
</ol>
</nav>Compatibility notes
The elements and aria-current are widely available. The chevrons use the rotate property (Baseline 2022) and :dir() (Baseline 2023); without the first a separator is an unturned corner, without the second the chevrons point right under RTL as well.
Examples
Current page as text
The pattern makes the current page a link with aria-current="page". When it should
not be a link, a <span aria-current="page"> is styled the same way.
<nav aria-label="Breadcrumb" data-slean="breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/table">Table</a></li>
<li><span aria-current="page">Pagination</span></li>
</ol>
</nav>Testing
apps/playground/tests/primitives/feedback-navigation.spec.tsPlaywright: roles, names and states in the accessibility tree, keyboard, RTL, reduced motion, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/breadcrumb/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/breadcrumb/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/breadcrumb.cssthe optional stylesheet