GrBreadcrumbs

Пакет: @feugene/granularityядроГруппа: Навигация

Берут, когда вложенность реальна.

Когда брать

  • вложенность реальна — каталог, файловая система, разделы документации: путь показывает, где пользователь;
  • путь длинный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": именно этот атрибут отвечает диктору на вопрос «где я». Если текущая страница должна оставаться кликабельной (перезагрузка раздела, якорь), включите linkCurrentaria-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

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefinedИмя лендмарка. Не задано — берётся из локали.
asGrBreadcrumbsLinkComponent | undefinedundefinedКомпонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia.
separatorstring | undefinedundefinedРазделитель между пунктами. Декоративен: диктору не читается.
maxItemsnumber | undefinedundefinedС какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать путь целиком.
itemsBeforeCollapsenumber | undefined1Сколько пунктов оставить в начале при схлопывании.
itemsAfterCollapsenumber | undefined1Сколько пунктов оставить в конце при схлопывании.
linkCurrentboolean | undefinedfalseПоследний пункт остаётся ссылкой (текущая страница кликабельна).
currentIndexnumber | undefinedundefinedКакой пункт — текущая страница. Не задан — последний. `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а путь показывает только родителей. Тогда `aria-current` не получает никто, и последний пункт остаётся обычной ссылкой.
expandLabelstring | undefinedundefinedi18n-метка кнопки раскрытия схлопнутого пути.
autoCollapseboolean | undefinedfalseСхлопывать по доступной ширине, а не по числу пунктов. Путь становится однострочным, середина уходит под «…» ровно настолько, насколько не влезает. Без пропа поведение прежнее: список переносится на вторую строку, а порог задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким потолком, ширина ужимает дальше.
itemsобязательныйGrBreadcrumbItem[]Путь от корня к текущей странице. Последний пункт — текущая страница.

Slots

SlotTypeОписание
item{ item: GrBreadcrumbItem; index: number; isCurrent: boolean; }Содержимое пункта целиком (иконка и подпись).
separatoranyРазделитель между пунктами.
ellipsis{ hiddenCount: number; expand: () => void; }Кнопка раскрытия схлопнутой середины.

Примеры 4

Схлопывание по доступному месту

autoCollapse считает по месту, а не по числу пунктов: путь становится однострочным и прячет середину ровно настолько, насколько не влезает.

Auto Collapse
<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", и именно так скринридер отвечает на вопрос «где я сейчас».

Basic
<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 решают, сколько уровней остаётся по краям. Клик по многоточию раскрывает путь на месте и переводит фокус на первый раскрытый пункт — кнопка исчезает вместе со схлопыванием.

Collapsed
<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 — путь читают и поиском, и диктором, и «домик» без имени сделал бы первый пункт для незрячего пустым.

Icons
<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), после раскрытия фокус переезжает на первый раскрытый пункт

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты