Components · Navigation
Breadcrumb
A trail of ancestor links ending in the current page — and, at size=page-title, the page heading itself (there is no separate page-title component).
Examples
Size
Separator
Content
Collapse
Ancestor trail · leading icons · last item is the current page
Page title · the current crumb IS the page heading (never a plain <h1>)
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-breadcrumb> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-breadcrumb.js'; // registers <aha-breadcrumb> (+ <aha-icon>)
</script>
<!-- Each item: { label, href?, icon?, disabled? }. An href-less / last entry is the current page.
separator="caret" (default) or "slash"; maxItems collapses the middle into an ellipsis toggle. -->
<aha-breadcrumb id="crumbs" separator="caret" maxItems="4"
items='[{"label":"Home","href":"/","icon":"system-house"},{"label":"Presentations","href":"/p","icon":"system-folder"},{"label":"Team offsite","href":"/p/1"},{"label":"Q3 deck","href":"/p/1/d"},{"label":"Editor"}]'>
</aha-breadcrumb>
<!-- PAGE TITLE — this is how you render a page heading. size="page-title" makes the current crumb a
real <h1> (heading-level picks h1–h6). Never hand-roll a plain <h1> for a page header — use this,
so the type scale, the ancestor trail, aria-current and the navigate event all come for free. -->
<aha-breadcrumb size="page-title" items='[{"label":"Presentations"}]'></aha-breadcrumb>
<!-- …or with the ancestor path in front of the heading -->
<aha-breadcrumb size="page-title"
items='[{"label":"Home","href":"/"},{"label":"Presentations","href":"/p"},{"label":"Team offsite"}]'>
</aha-breadcrumb>
<script>
// The last item is the current page (not a link); links emit a composed `navigate` event.
document.getElementById('crumbs').addEventListener('navigate', (e) => console.log('go to', e.detail.href));
</script>
import '@ahaslides-product/design/aha-breadcrumb'; // registers <aha-breadcrumb> (+ <aha-icon>)
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper for the composed `navigate` event; React 19 can bind onNavigate directly.
function AhaBreadcrumb({ items, onNavigate }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
const h = (e) => onNavigate?.(e.detail);
el.addEventListener('navigate', h);
return () => el.removeEventListener('navigate', h);
});
return <aha-breadcrumb ref={ref} items={JSON.stringify(items)} separator="caret" maxItems={4} />;
}
// usage — each item: { label, href?, icon?, disabled? }; the last / href-less entry is the current page.
<AhaBreadcrumb
items={[
{ label: 'Home', href: '/', icon: 'system-house' },
{ label: 'Presentations', href: '/p', icon: 'system-folder' },
{ label: 'Team offsite', href: '/p/1' },
{ label: 'Editor' },
]}
onNavigate={({ href }) => router.push(href)}
/>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-breadcrumb';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { computed } from 'vue';
// each item: { label, href?, icon?, disabled? }; the last / href-less entry is the current page.
const trail = [
{ label: 'Home', href: '/', icon: 'system-house' },
{ label: 'Presentations', href: '/p', icon: 'system-folder' },
{ label: 'Team offsite', href: '/p/1' },
{ label: 'Editor' },
];
const items = computed(() => JSON.stringify(trail));
function onNavigate(e) { router.push(e.detail.href); }
</script>
<template>
<aha-breadcrumb :items="items" separator="caret" :maxItems="4" @navigate="onNavigate" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
items | JSON [{label, href?, icon?, disabled?}] | [] | The trail; an entry with href is a link, the last / href-less entry is the current page, icon adds a leading DS glyph, disabled mutes it |
size | 'default' | 'mini' | 'page-title' | default | Scale of the trail. default 13/22 · mini 12/18 for sub-page nav; page-title renders the current crumb as the page heading (24/600 alone, 18/600 in a trail) |
heading-level | 1 | 2 | 3 | 4 | 5 | 6 | 1 | Heading tag used for the current crumb when size=page-title — an <h1> page title by default; drop to h2–h6 if the breadcrumb isn't the page's top heading |
separator | 'caret' | 'slash' | caret | Divider glyph between crumbs — the DS caret (default) or a slash |
maxItems | number | 0 | Collapse the middle into an ellipsis toggle when the trail is longer than N (0 = never) |
navigate | CustomEvent | — | Composed event on a link click, detail { index, href } |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-breadcrumb'; // registers <aha-breadcrumb>
Agent feed for this component (absolute, fetchable anywhere): breadcrumb.agent.json · breadcrumb.md · breadcrumb.llms.txt
When to use
When to use
- Breadcrumb (page-title) — the heading of a product page — size=page-title makes the current crumb the page <h1>, alone or at the end of its ancestor path. This is THE page title; do not hand-roll a plain <h1>/heading
- Breadcrumb (default / mini) — showing where a page sits in a hierarchy the user can climb back up, without heading-level prominence
- Screen heading — the page header ROW — a page title (via this breadcrumb) plus right-aligned action buttons
- Tabs — switching between sibling views at the same level — not a hierarchy
- Back button — a single linear step back is all that's needed
The page title is NOT a plain <h1> — it is <aha-breadcrumb size="page-title">, with just the current page as the only item (or the full ancestor path in front of it). A hand-rolled heading is the wrong answer: it drifts from the DS type scale and loses the built-in trail, aria-current, and navigate event. Keep breadcrumbs to the real ancestor path — don't pad them. The last item is the current page and is never a link. Labels are short nouns, sentence case.
Surfaces
editor dashboard admin settings
Spec
Type Plus Jakarta — default 13/22, mini 12/18, page-title 18/1.3 (solo title 24/1.3) · Link text-secondary #4A4A4A → hover color-primary #6A1EBB (motion-fast + ease-out) · Current text-default #1A1A1A, SemiBold 600, aria-current=page (stronger than links) · Page title size=page-title → current crumb is a real heading (<h1>, heading-level picks h1–h6); whole trail SemiBold 600; standalone (1 item) = Heading4 24/600/ls 0, in a trail = 18/1.3/ls 0.2 · Item icon optional leading <aha-icon>, currentColor — 12 mini / 14 default / 16 page-title · Disabled text-disabled #B5B5B5, not a link (aria-disabled) · Separator caret (<aha-icon name="system-caret-right">, icon-muted, 12 · 16 at page-title) or slash "/" · Collapse maxItems > trail → first + ellipsis toggle + trailing (maxItems−1) · Gap 8 between crumbs (4 at mini), 6 icon→label