GrBottomNav
Берут, когда мобильный интерфейс.
Обзор
Когда брать
- мобильный интерфейс — три-пять ключевых разделов в зоне большого пальца;
- на широком экране панель не нужна —
hideAboveубирает её, оставляя боковую навигацию; - у раздела есть счётчик —
badgeпоказывает непрочитанное прямо на иконке; - нужен лендмарк
navigation— корень рендерится тегом<nav>.
Когда взять другое
| Нужно | Берите |
|---|---|
| Разделов больше пяти | GrSidebar / GrDrawer |
| Верхняя панель приложения | GrNavbar |
| Разделы внутри одной страницы | GrTabs |
| Панель действий, а не навигация | GrButtonGroup |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrBottomNav
:items="items"
v-model="value"
hideAbove="none"
/>Примеры 4 у каждого — живое демо и его код
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Ступень размера: высота полосы, глиф и кегль подписи. Тач-таргет пункта остаётся 44×44 на любой ступени. |
ariaLabel | string | undefined | undefined | Имя лендмарка. Не задано — берётся из локали. |
as | string | Component | undefined | undefined | Компонент ссылки для пунктов с `to`: `RouterLink`, `NuxtLink`, `Link` Inertia. |
hideAbove | "sm" | "md" | "lg" | "none" | undefined | "sm" | Брейкпоинт, начиная с которого панель скрывается. `none` — видна всегда. |
position | "fixed" | "static" | undefined | "fixed" | `static` вынимает панель из фиксированного слоя — для встраивания в макет. |
modelValueобязательныйv-model | string | — | `value` активного пункта (`v-model`): он получает `aria-current="page"` и подсветку. |
itemsобязательный | GrBottomNavItem[] | — | Разделы панели: подпись, `value`, иконка, счётчик, `href` или `to`. Обычно три–пять ключевых разделов в зоне большого пальца. |
Слоты
| Slot | Type | Описание |
|---|---|---|
item |
| Содержимое пункта вместо иконки, подписи и счётчика. Корень пункта — тег, `aria-current`, `aria-disabled` и клик — остаётся за компонентом. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string): void | — |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrBottomNavItem, GrBottomNavProps, GrBottomNavEmits } from '@feugene/granularity/components/GrBottomNav'Файлdist/types/components/GrBottomNav/GrBottomNav.vue.d.ts
Пункт нижней навигации. Кроме подписи и значения всё опционально.
type GrBottomNavItem = {
label: string;
value: string;
/**
* Иконка: Vue-компонент либо класс иконки (`'i-lucide-home'` — правило для
* класса заводит приложение: фабрике движка или провайдером, см.
* `docs/installation.md#иконки`). Декоративна.
*/
icon?: string | Component;
/** Счётчик поверх пункта. Число озвучивается словами, строка — как есть. */
badge?: string | number;
/** Подпись счётчика для скринридера, сильнее локали. */
badgeLabel?: string;
disabled?: boolean;
href?: string;
/** Цель роутерной ссылки: уезжает в компонент из `as`. */
to?: unknown;
/** Доступное имя пункта, когда подписи недостаточно. */
ariaLabel?: string;
};GrBottomNav — нижняя навигация приложения: ряд разделов, прижатый к нижней
кромке экрана.
Активный пункт объявлен aria-current="page" и отличается не только цветом:
подложкой и весом подписи тоже.
interface GrBottomNavProps {
/** `value` активного пункта (`v-model`): он получает `aria-current="page"` и подсветку. */
modelValue: string;
/**
* Разделы панели: подпись, `value`, иконка, счётчик, `href` или `to`. Обычно
* три–пять ключевых разделов в зоне большого пальца.
*/
items: GrBottomNavItem[];
/** Компонент ссылки для пунктов с `to`: `RouterLink`, `NuxtLink`, `Link` Inertia. */
as?: string | Component;
/** Брейкпоинт, начиная с которого панель скрывается. `none` — видна всегда. */
hideAbove?: GrBottomNavHideAbove;
/** `static` вынимает панель из фиксированного слоя — для встраивания в макет. */
position?: GrBottomNavPosition;
/** Имя лендмарка. Не задано — берётся из локали. */
ariaLabel?: string;
/**
* Ступень размера: высота полосы, глиф и кегль подписи. Тач-таргет пункта
* остаётся 44×44 на любой ступени.
*/
size?: GrComponentSize;
}interface GrBottomNavEmits {
/** Выбран пункт — его `value`. Выключенный пункт событие не шлёт. */
(e: 'update:modelValue', value: string): void;
}Файлdist/types/components/GrBottomNav/defaults.d.ts
Пропы GrBottomNav, настраиваемые глобально через componentDefaults.
Шкала у панели своя по смыслу: ступень тянет высоту полосы, глиф и кегль
подписи, но не трогает тач-таргет пункта — он остаётся 44×44 на любой
ступени (см. grBottomNavStyles.ts).
interface GrBottomNavConfigurableProps {
size: GrComponentSize;
}Файлdist/types/components/GrBottomNav/config.d.ts
const grBottomNavConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBottomNav">;Файлdist/types/components/GrBottomNav/grBottomNavStyles.d.ts
type GrBottomNavHideAbove = typeof GR_BOTTOM_NAV_HIDE_ABOVE[number];type GrBottomNavPosition = typeof GR_BOTTOM_NAV_POSITIONS[number];Файлdist/types/components/GrBottomNav/safelist.d.ts
const grBottomNavSafelist: string[];Доступность
Паттерн APG
—
Клавиши
Enter / Space
| Клавиша | Действие |
|---|---|
| — | пункты — кнопки или ссылки, каждая своя остановка Tab |
| Enter / Space | перейти. Выключенный пункт рендерится <span>: он виден и объявлен через aria-disabled, но из таб-порядка выпадает и не активируется |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Разделы
<GrBottomNav
v-model="section"
:items="[
{ label: 'Лента', value: 'feed', icon: 'i-lucide-newspaper' },
{ label: 'Задачи', value: 'tasks', icon: 'i-lucide-check-square', badge: 12 },
{ label: 'Профиль', value: 'profile', icon: 'i-lucide-user' },
]"
/>| Поле пункта | Что делает |
|---|---|
label, value | подпись и значение v-model |
icon | Vue-компонент или класс иконки (i-lucide-* — правило для него заводит приложение: фабрике движка или провайдером, иначе — Vue-компонент; см. «Иконки»); декоративна |
badge | счётчик поверх пункта |
badgeLabel | подпись счётчика для скринридера, сильнее локали |
disabled | раздел виден, но недостижим |
href / to | пункт становится ссылкой |
ariaLabel | доступное имя, когда подписи недостаточно |
Где пользователь
Активный пункт объявлен aria-current="page" и отличается тремя признаками
сразу: подложкой, весом подписи и цветом. Одного цвета мало — он не виден при
монохромном зрении и не существует для диктора (WCAG 1.4.1).
Лендмарк всегда назван: без имени <nav> панели неотличим от основной
навигации страницы. Дефолт берётся из локали (gr.bottomNav.label), ariaLabel
его перебивает.
role="tab" компонент не использует: нижняя навигация переключает страницы, а
не панели внутри одного экрана, а tablist без tabpanel — сломанный паттерн.
Счётчик
Число рисуется декоративно, а рядом идёт визуально скрытая подпись: голое «12»
диктору ничего не сообщает. Числовое значение озвучивается через локаль
(gr.bottomNav.badge), строковое читается как есть, badgeLabel перебивает оба.
Ссылки
<GrBottomNav v-model="section" :as="RouterLink" :items="items" />Корневой тег пункта выбирается так: as (когда у пункта есть to) → <a href>
→ <button>. Ссылка даёт правый клик, «открыть в новой вкладке» и предзагрузку —
всё то, чего у кнопки нет. disabled делает корень <span>: недостижимый
раздел не должен оставаться в порядке Tab.
GrLink внутрь не импортируется намеренно: зависимость притащила бы потребителю
его CSS ради одного тега.
Видимость и слой
<GrBottomNav v-model="section" :items="items" hide-above="none" position="static" />hideAbove(smпо умолчанию, а такжеmd,lg,none) — брейкпоинт, с которого панель скрывается.noneнужен киоску и PWA, где нижняя навигация постоянна;position(fixedпо умолчанию,static) —staticвынимает панель из фиксированного слоя и кладёт обычным блоком в поток: так её можно встроить в макет.
В режиме fixed слой — --gr-z-bottom-nav (850): самый низкий в шкале, поэтому
открытая выпадашка, тултип или модалка панель перекрывают. Подробности —
../z-index.md.
Нижний отступ учитывает env(safe-area-inset-bottom) — на телефоне с жестовой
полосой подписи не уезжают под неё.
Состояния
disabled гасит пункт токеном --gr-disabled-fg, а не прозрачностью: opacity
разбавляет выверенные на AA цвета текста.
Размер
<GrBottomNav v-model="section" :items="items" size="lg" />
<GrConfigProvider size="sm">
…
</GrConfigProvider>Ступень тянет три вещи сразу:
| Ступень | Полоса | Глиф | Подпись |
|---|---|---|---|
xs | 48px | 16px | --gr-text-2xs |
sm | 56px | 20px | --gr-text-2xs |
md | 56px | 20px | --gr-text-xs |
lg | 64px | 24px | --gr-text-sm |
Чего ступень не трогает — размер самого пункта: min-width и min-height
остаются 44px на любой ступени. Тач-таргет мельче 44×44 — это провал WCAG 2.5.5,
и «сделать панель компактнее» не повод в него провалиться. Практическое
следствие: xs ужимает полосу за счёт воздуха вокруг содержимого, а не за счёт
области, в которую целится палец.
Значение приходит по общему правилу: локальный проп → componentDefaults →
глобальный size провайдера → md (см. ../sizes.md).
Свой пункт
Иконка, подпись и счётчик покрывают не всё: в мессенджере на месте иконки
профиля стоит аватар. Слот item отдаёт содержимое пункта целиком:
<GrBottomNav v-model="section" :items="items">
<template #item="{ item, active, disabled, badgeLabel }">
<GrAvatar v-if="item.value === 'me'" :size="24" alt="Ann Lee" />
<span v-else :class="item.icon" class="block h-5 w-5" aria-hidden="true" />
<span class="truncate leading-none">{{ item.label }}</span>
</template>
</GrBottomNav>Слот меняет содержимое, а не поведение: выбор тега (button / a /
компонент из as), aria-current, aria-disabled и обработка клика остаются
за компонентом. Поэтому кастомный пункт не может случайно перестать быть
навигацией.
Забирая разметку, вы забираете и озвучку счётчика: визуально скрытая подпись
рисуется вместе с бейджем. Готовая строка приходит слотом (badgeLabel) — её
достаточно положить в sr-only, локаль и badgeLabel пункта уже учтены.
Пропы этого примера