Use it for
Put BbBreadcrumbs at the top of a detail view, above the title. It answers two
questions at once: where am I, and how do I get back up.
Use something else when
BbTabs, if they are peer views of the same record: Overview, Activity, SettingsBbPagination, if the reader is stepping through a paged result set
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
Show one breadcrumb landmark per view.
The trail
Each item is { key, text } plus a navigation target. key must be unique: it
is also the name of that crumb's own slot.
<template>
<BbBreadcrumbs :items="trail" />
</template>
<script setup lang="ts">
import { BbBreadcrumbs } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
import { products } from '~/demo-data';
const product = products[0]!;
// Ancestors carry a target and render as links; the last item carries none —
// that is what makes it the current page, and the component marks it
// `aria-current="page"` for you. Keys are strings in v3.
const trail: BbBreadcrumbsItem[] = [
{ key: 'catalogue', text: 'Catalogue', href: '#default' },
{ key: 'category', text: product.category, href: '#default' },
{ key: 'product', text: product.name },
];
</script>
Leave the last item without a target. That is what makes it the current page:
it gets aria-current="page" and the theme renders it non-interactive. An href
there gives you a link nobody can click.
Coming from v2key must be a string
A numeric key that ran in v2 now fails type checking. The fix is String(id).
The runtime already stringified it, so nothing else changes.
Every crumb is a link
Give every crumb its own target: href, to, or an onClick.
- AppShell.vue
<template>
<div
class="flex w-full max-w-md flex-col gap-2 rounded-[var(--bb-radius)] border border-[color:var(--bb-border)] p-3"
>
<BbBreadcrumbs :items="trail" />
<ul class="divide-y divide-[color:var(--bb-border)] text-sm">
<li
v-for="entry in entries"
:key="entry.path"
class="flex items-center gap-2 py-1.5"
>
<BbButton
v-if="entry.children?.length"
prepend:icon="lucide:folder"
size="xs"
variant="ghost"
@click="open(entry)"
>
{{ entry.name }}
</BbButton>
<span
v-else
class="flex items-center gap-1.5 px-2 text-[color:var(--bb-text-muted)]"
>
<BbIcon icon="lucide:file" size="sm" />
{{ entry.name }}
</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbBreadcrumbs, BbButton, BbIcon } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
import type { FileNode } from '~/demo-data';
import { fileTree } from '~/demo-data';
const src = fileTree[0]!;
const path = ref<FileNode[]>([src, src.children![0]!]);
const entries = computed(() => path.value.at(-1)?.children ?? []);
const open = (folder: FileNode) => (path.value = [...path.value, folder]);
/*
* The hierarchy lives in this component's state, not in the URL, so ancestors
* get an `onClick` instead of an `href`. Everything else is unchanged: the
* crumbs are still real buttons, and the last one still has no target.
*/
const trail = computed<BbBreadcrumbsItem[]>(() =>
path.value.map((folder, index) => ({
key: folder.path,
text: folder.name,
...(index < path.value.length - 1
? {
onClick: () => {
path.value = path.value.slice(0, index + 1);
},
}
: {}),
}))
);
</script>
When the hierarchy lives in client state rather than in routes, give the
ancestors an onClick. The crumbs are still real buttons, and crumbs folded into
the overflow menu keep their handler. Never wrap the trail in your own
click-to-route handler.
disabled works at two levels. On one item, that crumb loses its href and
gains aria-disabled. On the component, every crumb and the overflow button
freeze together. Use it for a level the reader can no longer open, rather than
shipping a dead link.
Long trails fold themselves
You do not manage overflow.
Drag the frame narrower. The current page and its nearest ancestors stay put; the hidden levels keep their targets inside the menu.
<template>
<div class="flex flex-col gap-2">
<!-- A resizable frame stands in for a real layout: drag the bottom-right
corner and watch the earliest crumbs fold into the ellipsis menu. -->
<div
class="min-w-[12rem] resize-x overflow-auto rounded-[var(--bb-radius)] border border-[color:var(--bb-border)] p-3"
style="width: 22rem"
>
<BbBreadcrumbs :items="trail" />
</div>
<p class="text-xs text-[color:var(--bb-text-muted)]">
Drag the frame narrower. The current page and its nearest ancestors stay
put; the hidden levels keep their targets inside the menu.
</p>
</div>
</template>
<script setup lang="ts">
import { BbBreadcrumbs } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
// A file path with the workspace and the repository in front of it — six
// levels, more than a narrow column can hold. Pass the whole thing: pre-slicing
// the array to "manage" the width is what the estimator is there to avoid.
const segments = 'src/components/orders/OrderTable.vue'.split('/');
const trail: BbBreadcrumbsItem[] = [
{ key: 'workspace', text: 'Vantera', href: '#overflow' },
{ key: 'repository', text: 'acme-web', href: '#overflow' },
...segments.map((segment, index) => ({
key: `segment-${index}`,
text: segment,
...(index < segments.length - 1 ? { href: '#overflow' } : {}),
})),
];
</script>
When the trail outgrows its container the earliest crumbs fold into an ellipsis menu, root first. The current page and its nearest ancestors stay visible, and the hidden crumbs keep their targets.
Pass the full items array. Pre-slicing removes the keyboard-ready overflow
menu.
The fold point is estimated from label text. Start with offset-width for
icons, slots, and edge regions. Use estimation-bias, divider-width,
gap-width, or ellipsis-width only when their matching visual changes.
In a flex row, use class="min-w-0 flex-auto" and keep siblings shrink-0.
The full trail renders on the server, then folds after hydration.
Icons and edge regions
A fixed icon per crumb is an item field, not a slot: prepend:icon and
append:icon.
<template>
<!-- Everything in the edge regions is invisible to the width estimator, so
its total is declared once with `offset-width`: three 14px icons, the
leading hint, and the two trailing buttons. -->
<BbBreadcrumbs :items="trail" :offset-width="210">
<template #prepend>
<span class="text-xs text-[color:var(--bb-text-muted)]">You are here</span>
</template>
<template #append>
<span class="flex shrink-0 items-center gap-1.5">
<BbButton href="#edges" size="xs" variant="outline">Invite</BbButton>
<BbButton size="xs" variant="ghost" @click="exported = true">
{{ exported ? 'Exported' : 'Export' }}
</BbButton>
</span>
</template>
</BbBreadcrumbs>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbBreadcrumbs, BbButton } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
const exported = ref(false);
// A static glyph per crumb is an item field, not a slot. It renders outside
// the link, so it never inherits the link's text styling.
const trail: BbBreadcrumbsItem[] = [
{
key: 'settings',
text: 'Settings',
href: '#edges',
'prepend:icon': 'lucide:settings',
},
{
key: 'team',
text: 'Team',
href: '#edges',
'prepend:icon': 'lucide:users',
},
{ key: 'members', text: 'Members', 'prepend:icon': 'lucide:user-round' },
];
</script>
For varying content, use item:prepend or item:append. These slots are inside
the crumb link, so do not add interactive descendants. Account for custom
content with offset-width.
One crumb, its own slot
Every item also gets slots named after its key: #<key> replaces that
crumb's text, #<key>:prepend and #<key>:append what sits before and after
it.
<template>
<BbBreadcrumbs :items="trail">
<!--
The slot name is the item's key, normalized: `Order History` resolves
`#order_history`. A stale raw name renders the default label and warns
about nothing, so this is the one thing to check when a crumb slot
"stops working" after a v2 upgrade.
-->
<template #order_history="{ text }">
<span class="inline-flex items-center gap-1.5">
{{ text }}
<BbBadge size="sm" variant="secondary">{{ orders.length }}</BbBadge>
</span>
</template>
<!--
`#<key>:prepend` fills the space before one crumb's label: the current
record gets a status dot, and every other crumb stays plain.
-->
<template #order:prepend>
<span
aria-hidden="true"
class="inline-block size-2 shrink-0 rounded-full bg-[color:var(--bb-primary)]"
/>
</template>
</BbBreadcrumbs>
</template>
<script setup lang="ts">
import { BbBadge, BbBreadcrumbs } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
import { orders } from '~/demo-data';
const order = orders[0]!;
const trail: BbBreadcrumbsItem[] = [
{ key: 'dashboard', text: 'Dashboard', href: '#crumb-slot' },
{ key: 'Order History', text: 'Order history', href: '#crumb-slot' },
{ key: 'order', text: order.reference },
];
</script>
Use these slots when one level needs richer content. A crumb in the overflow menu follows the dropdown's rules.
The name is normalized: every run of spaces, hyphens and punctuation becomes
_, and the result is lowercased. An item keyed Order History is served by
#order_history, and one keyed order-history by the same slot.
Coming from v2slot names are normalized
A raw v2-era name matches nothing and silently renders the default label. That is the failure mode to look for after an upgrade.
Avoid slot-reserved keys such as prepend, append, divider, ellipsis,
activator, and item.
Divider and overflow button
The separator between crumbs is a slot, and only a slot.
<template>
<div class="max-w-64">
<!-- The replacement trigger is wider than the default `…`, so its width is
declared — otherwise the estimator reserves too little and the trail
clips instead of folding one crumb earlier. -->
<BbBreadcrumbs :ellipsis-width="40" :items="trail">
<template #divider>
<span class="text-[color:var(--bb-text-muted)]">/</span>
</template>
<template #ellipsis="{ overflowCount }">
<span class="text-xs font-medium">+{{ overflowCount }}</span>
</template>
</BbBreadcrumbs>
</div>
</template>
<script setup lang="ts">
import { BbBreadcrumbs } from 'bitboss-ui';
import type { BbBreadcrumbsItem } from 'bitboss-ui';
const trail: BbBreadcrumbsItem[] = [
{ key: 'library', text: 'Library', href: '#styling' },
{ key: 'photos', text: 'Photos', href: '#styling' },
{ key: 'albums', text: 'Albums', href: '#styling' },
{ key: 'summer', text: 'Summer 2026' },
];
</script>
Dividers are decorative and stay aria-hidden, so never encode meaning in one.
If yours is visually wider than the default, declare it with divider-width.
The ellipsis slot receives overflowCount and keeps the accessible button and
menu behavior. Pair a wider replacement with ellipsis-width.
Coming from v2divider removed
There is no divider prop in v3, and passing it now just leaks an attribute onto
the markup. The default separator is a small chevron, not a /. The default of
divider-width moved from 5 to 16, which matters only if you had tuned the
old value.