GrBreadcrumbs
Shows where the user is and lets them step back up the path.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the nesting is real — a catalogue, a file system, sections of documentation: the path shows where the user is;
- the path is long —
maxItemsandautoCollapsefold the middle, leaving the beginning and the end; - every level can be returned to — each ancestor is a link, the last item is the current page;
- the router is already connected — the links are drawn by
GrLink, so the way of navigating is the same as everywhere.
When to take something else
| Need | Take |
|---|---|
| Steps of a process rather than levels | GrTimeline |
| Sections of one level | GrTabs |
| The main navigation of the application | GrSidebar / GrNavbar |
| A single step back | GrButton / GrLink |
A flat site needs no breadcrumbs: a path of one level says nothing and takes up space. They pay off where the user can end up on the fourth level and not remember the way.
The router
<GrBreadcrumbs :as="RouterLink" :items="items" />
const items = [
{ label: 'Projects', to: '/projects' },
{ label: 'Granularity', to: '/projects/granularity' },
{ label: 'Settings' },
]
as sets the link component for every item at once, and to travels into it as a
prop through the attrs of GrLink. Without as an item with href renders as
an ordinary <a>.
The current page
The last item is not a link and is declared aria-current="page": it is that
attribute that answers the screen reader’s question “where am I”. If the current
page has to stay clickable (reloading a section, an anchor), switch on
linkCurrent — aria-current is kept in that case.
An intermediate item can be switched off (disabled: true) — it becomes text, but
does not get aria-current.
A long path
<GrBreadcrumbs :items="path" :max-items="4" :items-before-collapse="1" :items-after-collapse="2" />
The middle folds into a ”…” button, and as many levels stay at the edges as
itemsBeforeCollapse/itemsAfterCollapse ask for. The tail matters more than the
head: “where am I now” is read from the right, so when there is not enough room it
is the middle that is sacrificed.
The component will not hide a single item — the button would take up just as much room.
A click on the ellipsis expands the path in place, without a dropdown menu. The
button disappears in the process, so the focus is moved to the first expanded item:
otherwise it would end up on <body>. Changing items (a new page) folds the path
again.
The layout can also be computed from the outside — resolveBreadcrumbsLayout is
exported as a pure function.
Folding by the room available
<GrBreadcrumbs :items="path" auto-collapse />
maxItems counts items, and on a narrow screen a number is a poor predictor: three
short levels fit, two long ones do not. autoCollapse measures: the path becomes
single-line, and the middle goes under the ”…” by exactly as much as does not
fit.
What changes along with the mode:
| without the prop | autoCollapse | |
|---|---|---|
| a long path | wraps onto a second line | stays on one line |
| the threshold | maxItems (the number of items) | the width of the container |
| the tail | itemsAfterCollapse | as much as fits, but not less than one |
The head is not squeezed: itemsBeforeCollapse is usually the root item, and it is
cheap. The tail is not squeezed below one item: the last one answers the question
“where am I now”, and the path “Home / …” is useless.
The props are compatible: maxItems remains a hard ceiling, and the width squeezes
further.
The first frame in this mode is the full path: the widths of the labels are taken
from it, and there is nowhere to get them before the render. After that the
recalculation runs on ResizeObserver and costs one arithmetic operation — the
labels do not depend on the width of the container.
The arithmetic is separated out: resolveBreadcrumbsFit is exported as a pure
function if the decision has to be made from the outside.
The separator and the size
separator (/ by default) and size are read from GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { separator: '›' } }">
The separator is decorative — it lives in a list item of its own with
aria-hidden: the structure of the path is told to the screen reader by the list
itself, and it would read the ”/” aloud.
The icon of an item
The icon field is decorative (aria-hidden) and accepts a Vue component or an
icon class from your UnoCSS build (see “Icons”).
iconOnly shows only the icon — the classic “house” at the start of the path:
<GrBreadcrumbs :items="[{ label: 'Home', href: '/', icon: 'i-lucide-house', iconOnly: true }, …]" />
The label is not thrown away and does not move into aria-label; it is hidden with
sr-only. The reason lies in who reads the path: a search engine and a screen
reader read it as text, and an item without a name is empty for a blind user — they
hear “link” without knowing where to.
Without icon the field is ignored, and in development the component warns:
hiding the label without showing anything in its place means erasing the item — in
the markup everything is in place, and the loss is visible to the eye alone.
Slots
| Slot | What it replaces |
|---|---|
item | the content of an item (item, index, isCurrent) |
separator | the separator |
ellipsis | the expand button (hiddenCount, expand) |
Playground 10
Loading…
<GrBreadcrumbs />Install
npm i @feugene/granularityImport
import { GrBreadcrumbs } from '@feugene/granularity/components/GrBreadcrumbs'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | The name of the landmark. If it is not set, it comes from the locale. |
as | GrBreadcrumbsLinkComponent | undefined | undefined | The link component for every item: `RouterLink`, `NuxtLink`, Inertia’s `Link`. |
separator | string | undefined | undefined | The separator between items. Decorative: it is not read to a screen reader. |
maxItems | number | undefined | undefined | From how many items the middle folds into a "…". `0` or unset — show the whole path. |
itemsBeforeCollapse | number | undefined | 1 | How many items to leave at the start when folding. |
itemsAfterCollapse | number | undefined | 1 | How many items to leave at the end when folding. |
linkCurrent | boolean | undefined | false | The last item stays a link (the current page is clickable). |
currentIndex | number | undefined | undefined | Which item is the current page. Unset — the last one. `-1` means the current page is not in the path: its name stands in the `h1`, and the path shows the ancestors only. Then nobody gets `aria-current`, and the last item stays an ordinary link. |
expandLabel | string | undefined | undefined | The i18n label of the button that expands a folded path. |
autoCollapse | boolean | undefined | false | Fold by the available width rather than by the number of items. The path becomes single-line, and the middle goes under the "…" by exactly as much as does not fit. Without the prop the behaviour is as before: the list wraps onto a second line, and the threshold is set by `maxItems`. Together the props are compatible — `maxItems` remains a hard ceiling, and the width squeezes further. |
itemsrequired | GrBreadcrumbItem[] | — | The path from the root to the current page. The last item is the current page. |
Slots
| Slot | Type | Description |
|---|---|---|
item | { item: GrBreadcrumbItem; index: number; isCurrent: boolean; } | The content of an item as a whole (the icon and the label). |
separator | any | The separator between items. |
ellipsis | { hiddenCount: number; expand: () => void; } | The button that expands the folded middle. |
Examples 4
Collapsing by available width
<script setup lang="ts">
import { ref } from 'vue'
import { GrBreadcrumbs, GrSegmented, type GrBreadcrumbItem } from '@feugene/granularity'
const path: GrBreadcrumbItem[] = [
{ label: 'Storage', href: '#/storage' },
{ label: 'Workspaces', href: '#/storage/workspaces' },
{ label: 'Design system', href: '#/storage/workspaces/design-system' },
{ label: 'Releases', href: '#/storage/workspaces/design-system/releases' },
{ label: '0.15.0', href: '#/storage/workspaces/design-system/releases/0-15-0' },
{ label: 'CHANGELOG.md' },
]
// Ширина контейнера, а не окна: схлопывание считается по доступному месту,
// поэтому увидеть его можно не трогая размер браузера.
const width = ref('520')
const widths = [
{ value: '260', label: 'Narrow' },
{ value: '380', label: 'Medium' },
{ value: '520', label: 'Wide' },
]
</script>
<template>
<div class="grid gap-4">
<GrSegmented v-model="width" :options="widths" size="sm" class="justify-self-start" />
<div
class="rounded-[var(--gr-radius-md)] border border-dashed border-[var(--gr-brd)] p-3"
:style="{ width: `${width}px`, maxWidth: '100%' }"
>
<GrBreadcrumbs :items="path" auto-collapse />
</div>
</div>
</template>Path to the current page
<script setup lang="ts">
import { GrBreadcrumbs, type GrBreadcrumbItem } from '@feugene/granularity'
const path: GrBreadcrumbItem[] = [
{ label: 'Dashboard', href: '#/dashboard' },
{ label: 'Projects', href: '#/projects' },
{ label: 'Granularity', href: '#/projects/granularity' },
{ label: 'Settings' },
]
</script>
<template>
<GrBreadcrumbs :items="path" />
</template>Long path with a collapsed middle
<script setup lang="ts">
import { GrBreadcrumbs, type GrBreadcrumbItem } from '@feugene/granularity'
// Длинный путь из файлового менеджера: середина сворачивается, начало и конец
// остаются на виду.
const path: GrBreadcrumbItem[] = [
{ label: 'Storage', href: '#/storage' },
{ label: 'Workspaces', href: '#/storage/workspaces' },
{ label: 'Design system', href: '#/storage/workspaces/design-system' },
{ label: 'Releases', href: '#/storage/workspaces/design-system/releases' },
{ label: '0.15.0', href: '#/storage/workspaces/design-system/releases/0-15-0' },
{ label: 'CHANGELOG.md' },
]
</script>
<template>
<GrBreadcrumbs
:items="path"
:max-items="4"
:items-before-collapse="1"
:items-after-collapse="2"
/>
</template>Icons, custom separator and size
Первый пункт второго и третьего пути — iconOnly: видна только иконка. Подпись при этом не выброшена, а спрятана sr-only — путь читают и поиском, и диктором, и «домик» без имени сделал бы первый пункт для незрячего пустым.
<script setup lang="ts">
import { GrBreadcrumbs, type GrBreadcrumbItem } from '@feugene/granularity'
const path: GrBreadcrumbItem[] = [
{ label: 'Home', href: '#/', icon: 'i-lucide-house' },
{ label: 'Team', href: '#/team', icon: 'i-lucide-users' },
{ label: 'Ada Lovelace', icon: 'i-lucide-user' },
]
/**
* Все четыре вида пункта в одном пути: пункт-иконка, ссылка без иконки, ссылка
* с иконкой и обычный пункт без ссылки.
*/
const mixed: GrBreadcrumbItem[] = [
{ label: 'Главная', href: '#/', icon: 'i-lucide-house', iconOnly: true },
{ label: 'Проекты', href: '#/projects' },
{ label: 'Гранулярность', href: '#/projects/granularity', icon: 'i-lucide-box' },
{ label: 'Настройки' },
]
/** Короткий путь второго уровня: домик и страница. */
const short: GrBreadcrumbItem[] = [
{ label: 'Главная', href: '#/', icon: 'i-lucide-house', iconOnly: true },
{ label: 'Профиль' },
]
</script>
<template>
<div class="grid gap-5">
<GrBreadcrumbs :items="path" separator="›" size="lg" />
<GrBreadcrumbs :items="mixed" size="lg" />
<GrBreadcrumbs :items="short" size="lg" />
<p class="text-sm text-[var(--gr-muted-fg)]">
Первый пункт второго и третьего пути — <code>iconOnly</code>: видна только иконка. Подпись при
этом не выброшена, а спрятана <code>sr-only</code> — путь читают и поиском, и диктором, и
«домик» без имени сделал бы первый пункт для незрячего пустым.
</p>
</div>
</template>Accessibility
- APG pattern
breadcrumb