GrBreadcrumbs
Берут, когда вложенность реальна.
Когда брать
- вложенность реальна — каталог, файловая система, разделы документации: путь показывает, где пользователь;
- путь длинный —
maxItemsиautoCollapseсворачивают середину, оставляя начало и конец; - на любой уровень можно вернуться — каждый предок ссылка, последний пункт — текущая страница;
- роутер уже подключён — ссылки рисует
GrLink, поэтому способ навигации тот же, что везде.
Когда взять другое
| Нужно | Берите |
|---|---|
| Шаги процесса, а не уровни | GrTimeline |
| Разделы одного уровня | GrTabs |
| Основная навигация приложения | GrSidebar / GrNavbar |
| Один переход назад | GrButton / GrLink |
Плоский сайт хлебных крошек не требует: путь из одного уровня ничего не сообщает, а место занимает. Они окупаются там, где пользователь может оказаться на четвёртом уровне и не помнить дороги.
Роутер
<GrBreadcrumbs :as="RouterLink" :items="items" />
const items = [
{ label: 'Проекты', to: '/projects' },
{ label: 'Гранулярность', to: '/projects/granularity' },
{ label: 'Настройки' },
]
as задаёт компонент ссылки на все пункты сразу, to уезжает в него пропом
через attrs GrLink. Без as пункт со href рендерится обычным <a>.
Текущая страница
Последний пункт — не ссылка и объявлен aria-current="page": именно этот
атрибут отвечает диктору на вопрос «где я». Если текущая страница должна
оставаться кликабельной (перезагрузка раздела, якорь), включите linkCurrent —
aria-current при этом сохраняется.
Промежуточный пункт можно выключить (disabled: true) — он станет текстом, но
aria-current не получит.
Длинный путь
<GrBreadcrumbs :items="path" :max-items="4" :items-before-collapse="1" :items-after-collapse="2" />
Середина сворачивается в кнопку «…», по краям остаётся столько уровней, сколько
просят itemsBeforeCollapse/itemsAfterCollapse. Хвост важнее головы: «где я
сейчас» читается справа, поэтому при нехватке места жертвуют серединой.
Прятать один пункт компонент не станет — кнопка заняла бы столько же места.
Клик по многоточию раскрывает путь на месте, без выпадающего меню. Кнопка
при этом исчезает, поэтому фокус переводится на первый раскрытый пункт: иначе он
уехал бы на <body>. Смена items (новая страница) снова схлопывает путь.
Раскладку можно посчитать и снаружи — resolveBreadcrumbsLayout экспортируется
как чистая функция.
Схлопывание по доступному месту
<GrBreadcrumbs :items="path" auto-collapse />
maxItems считает пункты, а на узком экране число — плохой предиктор: три
коротких уровня влезают, два длинных — нет. autoCollapse меряет: путь
становится однострочным, и середина уходит под «…» ровно настолько,
насколько не влезает.
Что меняется вместе с режимом:
| без пропа | autoCollapse | |
|---|---|---|
| длинный путь | переносится на вторую строку | остаётся в одной строке |
| порог | maxItems (число пунктов) | ширина контейнера |
| хвост | itemsAfterCollapse | столько, сколько влезло, но не меньше одного |
Голова не ужимается: itemsBeforeCollapse — обычно корневой пункт, и он дёшев.
Хвост не ужимается ниже одного пункта: последний отвечает на вопрос «где я
сейчас», и путь «Главная / …» бесполезен.
Пропы совместимы: maxItems остаётся жёстким потолком, ширина ужимает дальше.
Первый кадр в этом режиме — полный путь: ширины подписей снимаются с него, а
взять их до рендера неоткуда. Дальше пересчёт идёт по ResizeObserver и стоит
одну арифметическую операцию — подписи от ширины контейнера не зависят.
Арифметика вынесена отдельно: resolveBreadcrumbsFit экспортируется как чистая
функция, если решение нужно принять снаружи.
Разделитель и размер
separator (по умолчанию /) и size читаются из GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { separator: '›' } }">
Разделитель декоративен — он в отдельном элементе списка с aria-hidden:
структуру пути диктору сообщает сам список, а «/» он бы читал вслух.
Иконка пункта
Поле icon декоративно (aria-hidden) и принимает Vue-компонент либо класс
иконки вашей UnoCSS-сборки (см. «Иконки»).
iconOnly показывает только иконку — классический «домик» в начале пути:
<GrBreadcrumbs :items="[{ label: 'Главная', href: '/', icon: 'i-lucide-house', iconOnly: true }, …]" />
Подпись при этом не выбрасывается и не переезжает в aria-label, а прячется
sr-only. Причина в том, кто читает путь: поисковик и диктор читают его текстом,
и пункт без имени для незрячего пустой — он слышит «ссылка», не зная куда.
Без icon поле игнорируется, а в разработке компонент предупреждает: спрятать
подпись, не показав ничего взамен, значит стереть пункт — в разметке при этом всё
на месте, и пропажу видно только глазами.
Слоты
| Слот | Что заменяет |
|---|---|
item | содержимое пункта (item, index, isCurrent) |
separator | разделитель |
ellipsis | кнопку раскрытия (hiddenCount, expand) |
Playground 10
Загружается…
<GrBreadcrumbs />Установка
npm i @feugene/granularityИмпорт
import { GrBreadcrumbs } from '@feugene/granularity/components/GrBreadcrumbs'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | Имя лендмарка. Не задано — берётся из локали. |
as | GrBreadcrumbsLinkComponent | undefined | undefined | Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. |
separator | string | undefined | undefined | Разделитель между пунктами. Декоративен: диктору не читается. |
maxItems | number | undefined | undefined | С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать путь целиком. |
itemsBeforeCollapse | number | undefined | 1 | Сколько пунктов оставить в начале при схлопывании. |
itemsAfterCollapse | number | undefined | 1 | Сколько пунктов оставить в конце при схлопывании. |
linkCurrent | boolean | undefined | false | Последний пункт остаётся ссылкой (текущая страница кликабельна). |
currentIndex | number | undefined | undefined | Какой пункт — текущая страница. Не задан — последний. `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а путь показывает только родителей. Тогда `aria-current` не получает никто, и последний пункт остаётся обычной ссылкой. |
expandLabel | string | undefined | undefined | i18n-метка кнопки раскрытия схлопнутого пути. |
autoCollapse | boolean | undefined | false | Схлопывать по доступной ширине, а не по числу пунктов. Путь становится однострочным, середина уходит под «…» ровно настолько, насколько не влезает. Без пропа поведение прежнее: список переносится на вторую строку, а порог задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким потолком, ширина ужимает дальше. |
itemsобязательный | GrBreadcrumbItem[] | — | Путь от корня к текущей странице. Последний пункт — текущая страница. |
Slots
| Slot | Type | Описание |
|---|---|---|
item | { item: GrBreadcrumbItem; index: number; isCurrent: boolean; } | Содержимое пункта целиком (иконка и подпись). |
separator | any | Разделитель между пунктами. |
ellipsis | { hiddenCount: number; expand: () => void; } | Кнопка раскрытия схлопнутой середины. |
Примеры 4
Схлопывание по доступному месту
autoCollapse считает по месту, а не по числу пунктов: путь становится однострочным и прячет середину ровно настолько, насколько не влезает.
<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>Путь до текущей страницы
Базовый сценарий: путь от корня до текущей страницы. Последний пункт — не ссылка: он помечен aria-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>Длинный путь со свёрнутой серединой
max-items сворачивает середину пути в «…», items-before-collapse и items-after-collapse решают, сколько уровней остаётся по краям. Клик по многоточию раскрывает путь на месте и переводит фокус на первый раскрытый пункт — кнопка исчезает вместе со схлопыванием.
<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>Иконки, свой разделитель и размер
Иконка пункта задаётся классом в поле icon и остаётся декоративной, разделитель меняется пропом separator, размер — общей шкалой пакета или глобально через GrConfigProvider.
Первый пункт второго и третьего пути — 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>Доступность
- Паттерн APG
breadcrumb- Клавиши
- Tab по ссылкам пути; кнопка «…» — обычная кнопка (
Enter/Space), после раскрытия фокус переезжает на первый раскрытый пункт