GrBottomNav

  • ядро
  • Навигация
  • API · 7 props
  • ⌨ клавиши

Берут, когда мобильный интерфейс.

import { GrBottomNav } from '@feugene/granularity/components/GrBottomNav'

Обзор

Когда брать

  • мобильный интерфейс — три-пять ключевых разделов в зоне большого пальца;
  • на широком экране панель не нужна — hideAbove убирает её, оставляя боковую навигацию;
  • у раздела есть счётчик — badge показывает непрочитанное прямо на иконке;
  • нужен лендмарк navigation — корень рендерится тегом <nav>.

Когда взять другое

НужноБерите
Разделов больше пятиGrSidebar / GrDrawer
Верхняя панель приложенияGrNavbar
Разделы внутри одной страницыGrTabs
Панель действий, а не навигацияGrButtonGroup

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 4 у каждого — живое демо и его код

  • Разделы банковского приложения

    v-model переключает экран, а активный раздел отличается сразу подложкой, весом и цветом. badge считает, что ждёт в разделе, а badgeLabel называет это словами — одобрите оба платежа, и счётчик уйдёт с полосы. Раздел с disabled виден, но выпадает из порядка Tab: инвестиции открываются после проверки личности. В приложении полоса прижата к краю и прячется с sm; здесь position="static" и hide-above="none" кладут её в рамку.

    Accounts
    Main account · DE89 •••• 4410€12,480.50
    Contoso LtdToday+€2,400.00
    Office rentYesterday−€1,850.00
    Tailspin Toys3 Oct+€310.00
  • Аватар вместо иконки

    Слот item заменяет содержимое каждого пункта, и «Me» получает аватар — туда, где его ставит мессенджер. Тег, aria-current и клик остаются за компонентом, а слот отдаёт badgeLabel — готовую подпись счётчика. После 99 бейдж — строка «99+», а badgeLabel сохраняет настоящие 128; «Me» сам по себе мало что говорит, и у пункта свой ariaLabel.

    Chats
    GWGrace WuThe VAT return is ready for review14:02
    FTFinance teamOmar: invoices for October are out13:47
    LRLinus RayCan we move the call to Friday?11:30
    MOMina OrtizThanks, received!Mon
  • Разделы — это маршруты

    Когда у каждого раздела есть адрес, состоянием владеет роутер: у пунктов to, as рисует их компонентом ссылки приложения — RouterLink, NuxtLink, Link из Inertia, — а model-value следует за текущим маршрутом вместо v-model. Ссылка сохраняет правый клик и «открыть в новой вкладке», которых у кнопки нет.

    Northwind Office#/orders
    Order #4815 · out for deliveryArrives today, 14:00–16:00
    Order #4790 · delivered28 September
  • Киоск держит полосу на любой ширине

    На экране самообслуживания нижняя полоса — главная навигация, а не мобильная замена: hide-above="none" оставляет её на широком дисплее. size="lg" растит полосу, глиф и подпись вместе — для читателя на расстоянии вытянутой руки, а тач-таргет остаётся 44×44 на любой ступени.

    Order here · Harbour Café1 in the cart · €3.20
    Flat white€3.20
    Cinnamon bun€2.90
    Tomato soup€5.50
    Club sandwich€7.40

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefinedСтупень размера: высота полосы, глиф и кегль подписи. Тач-таргет пункта остаётся 44×44 на любой ступени.
ariaLabelstring | undefinedundefinedИмя лендмарка. Не задано — берётся из локали.
asstring | Component | undefinedundefinedКомпонент ссылки для пунктов с `to`: `RouterLink`, `NuxtLink`, `Link` Inertia.
hideAbove"sm" | "md" | "lg" | "none" | undefined"sm"Брейкпоинт, начиная с которого панель скрывается. `none` — видна всегда.
position"fixed" | "static" | undefined"fixed"`static` вынимает панель из фиксированного слоя — для встраивания в макет.
modelValueобязательныйv-modelstring—`value` активного пункта (`v-model`): он получает `aria-current="page"` и подсветку.
itemsобязательныйGrBottomNavItem[]—Разделы панели: подпись, `value`, иконка, счётчик, `href` или `to`. Обычно три–пять ключевых разделов в зоне большого пальца.

Слоты

SlotTypeОписание
item
{ item: GrBottomNavItem; active: boolean; disabled: boolean;{ item: GrBottomNavItem; active: boolean; disabled: boolean; badgeLabel: string | undefined; }
Содержимое пункта вместо иконки, подписи и счётчика. Корень пункта — тег, `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

GrBottomNavItemtype

Пункт нижней навигации. Кроме подписи и значения всё опционально.

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;
};
GrBottomNavPropsinterface

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;
}
GrBottomNavEmitsinterface
interface GrBottomNavEmits {
    /** Выбран пункт — его `value`. Выключенный пункт событие не шлёт. */
    (e: 'update:modelValue', value: string): void;
}

Файлdist/types/components/GrBottomNav/defaults.d.ts

GrBottomNavConfigurablePropsinterface

Пропы GrBottomNav, настраиваемые глобально через componentDefaults. Шкала у панели своя по смыслу: ступень тянет высоту полосы, глиф и кегль подписи, но не трогает тач-таргет пункта — он остаётся 44×44 на любой ступени (см. grBottomNavStyles.ts).

interface GrBottomNavConfigurableProps {
    size: GrComponentSize;
}

Файлdist/types/components/GrBottomNav/config.d.ts

grBottomNavConfigconst
const grBottomNavConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBottomNav">;

Файлdist/types/components/GrBottomNav/grBottomNavStyles.d.ts

GrBottomNavHideAbovetype
type GrBottomNavHideAbove = typeof GR_BOTTOM_NAV_HIDE_ABOVE[number];
GrBottomNavPositiontype
type GrBottomNavPosition = typeof GR_BOTTOM_NAV_POSITIONS[number];

Файлdist/types/components/GrBottomNav/safelist.d.ts

grBottomNavSafelistconst
const grBottomNavSafelist: string[];

Доступность

Паттерн APG

—

Клавиши

Enter / Space

КлавишаДействие
—пункты — кнопки или ссылки, каждая своя остановка Tab
Enter / Spaceперейти. Выключенный пункт рендерится <span>: он виден и объявлен через aria-disabled, но из таб-порядка выпадает и не активируется

Использование из прозы библиотеки

Разделы
vue
<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
iconVue-компонент или класс иконки (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 перебивает оба.

Ссылки
vue
<GrBottomNav v-model="section" :as="RouterLink" :items="items" />

Корневой тег пункта выбирается так: as (когда у пункта есть to) → <a href> → <button>. Ссылка даёт правый клик, «открыть в новой вкладке» и предзагрузку — всё то, чего у кнопки нет. disabled делает корень <span>: недостижимый раздел не должен оставаться в порядке Tab.

GrLink внутрь не импортируется намеренно: зависимость притащила бы потребителю его CSS ради одного тега.

Видимость и слой
vue
<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 цвета текста.

Размер
vue
<GrBottomNav v-model="section" :items="items" size="lg" />

<GrConfigProvider size="sm">
…
</GrConfigProvider>

Ступень тянет три вещи сразу:

СтупеньПолосаГлифПодпись
xs48px16px--gr-text-2xs
sm56px20px--gr-text-2xs
md56px20px--gr-text-xs
lg64px24px--gr-text-sm

Чего ступень не трогает — размер самого пункта: min-width и min-height остаются 44px на любой ступени. Тач-таргет мельче 44×44 — это провал WCAG 2.5.5, и «сделать панель компактнее» не повод в него провалиться. Практическое следствие: xs ужимает полосу за счёт воздуха вокруг содержимого, а не за счёт области, в которую целится палец.

Значение приходит по общему правилу: локальный проп → componentDefaults → глобальный size провайдера → md (см. ../sizes.md).

Свой пункт

Иконка, подпись и счётчик покрывают не всё: в мессенджере на месте иконки профиля стоит аватар. Слот item отдаёт содержимое пункта целиком:

vue
<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 пункта уже учтены.