GrBadgeWrap
Берут, когда счётчик относится к контролу.
Когда брать
- счётчик относится к контролу — непрочитанные на иконке почты, товары в корзине, задачи на вкладке;
- важен сам факт, а не число —
dotпоказывает точку без цифры; - число может быть большим —
maxсворачивает его в «99+»; - ноль надо показать —
showZero; по умолчанию счётчик исчезает, а не рисует «0».
Когда взять другое
| Нужно | Берите |
|---|---|
| Метка стоит в потоке, а не поверх контрола | GrBadge |
| Статус присутствия на аватаре | GrAvatar |
| Счётчик — часть вкладки | GrTabs с badge |
| Счётчик — часть раздела навигации | GrBottomNav с badge |
Счётчик
<GrBadgeWrap :value="3">
<GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>
Число рисуется декоративно, а рядом идёт визуально скрытая подпись — иначе
диктор читал бы «Inbox, 3» без единицы измерения либо не читал счётчик вовсе.
Текст берётся из локали (gr.badgeWrap.count), ariaLabel перебивает его.
Ноль по умолчанию не показывается: пустой кружок на иконке читается как ошибка.
showZero возвращает его, когда ноль — осмысленное состояние («0 черновиков»).
Обрезка крупных чисел
<GrBadgeWrap :value="120" :max="99" />
Видно «99+», а озвучивается настоящее число: «120 непрочитанных». «99 плюс» пользователю ничего не сообщает.
Функция доступна отдельно: import { formatBadgeValue } from '@feugene/granularity'.
Точка
<GrBadgeWrap dot aria-label="Есть непрочитанные" />
Точка не несёт числа и остаётся декоративной, пока ей не задали ariaLabel, —
тогда факт события объявляется словом. Годится, когда важно «требует внимания»,
а не «сколько именно».
Анимация счётчика
<GrBadgeWrap :value="unread" animate>
<GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>
animate отмечает «попом» то, о чём пользователь ещё не знает: появление
счётчика и рост числа. Убыль — след его собственного действия, и подсвечивать
её значит мигать бейджем на каждом прочитанном письме.
| Переход | Что происходит |
|---|---|
| счётчика не было → появился | «поп» |
| число выросло | «поп» |
| число уменьшилось | молча |
| счётчик исчез | молча |
| первый рендер страницы | молча |
Строковое значение (value="NEW") порядка не имеет, поэтому любая его смена
считается событием.
Пачка изменений даёт один «поп»: пока анимация играет, новое значение её не перезапускает. Пять писем, пришедших за секунду, отмечаются одним движением, а не мельтешением.
По умолчанию animate выключен. Уместна ли анимация, знает приложение: счётчик,
который меняется раз в час, и счётчик, который меняется постоянно, — разные
случаи, и библиотека выбирает за потребителя тихий.
Точка (dot) в анимации не участвует: у неё нет значения, и «рост» для неё не
определён.
Движение подчиняется prefers-reduced-motion через глобальный кламп пакета —
подробности в motion.md. Длительность и кривая берутся из
--gr-duration-base и --gr-ease-out, то есть настраиваются темой вместе со
всем остальным движением.
Тон и положение
| Проп | Значения | По умолчанию |
|---|---|---|
tone | вся шкала GR_TONES (neutral, primary, success, warning, danger, info, slate, azure) | danger |
placement | top-right | top-left | bottom-right | bottom-left | top-right |
Цвет текста берётся из парного токена --gr-<tone>-fg, а не из белого литерала:
бейдж переживает смену темы.
Смещение от угла настраивается переменными — темой или инлайн-стилем:
<GrBadgeWrap :value="3" style="--gr-badge-wrap-offset-x: -0.75rem" />
| Переменная | По умолчанию (счётчик / точка) |
|---|---|
--gr-badge-wrap-offset-x | -0.5rem / -0.25rem |
--gr-badge-wrap-offset-y | -0.5rem / -0.25rem |
Почему не `GrBadge` внутри
Обёртка рисует свой минимальный бейдж намеренно: иначе каждый счётчик тянул бы за собой полноразмерный компонент со своей шкалой размеров и радиусов ради кружка в 20 пикселей.
Playground 7
Загружается…
<GrBadgeWrap />Установка
npm i @feugene/granularityИмпорт
import { GrBadgeWrap } from '@feugene/granularity/components/GrBadgeWrap'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | "danger" | — |
ariaLabel | string | undefined | undefined | Подпись счётчика для скринридера. Не задана — берётся из локали. |
value | string | number | undefined | undefined | — |
dot | boolean | undefined | false | Точка вместо числа: чистая декорация, если не задан `ariaLabel`. |
max | number | undefined | undefined | Порог: значения больше него рисуются как «{max}+». |
showZero | boolean | undefined | false | Показывать ли нулевое значение. По умолчанию ноль скрыт. |
placement | "top-right" | "top-left" | "bottom-right" | "bottom-left" | undefined | "top-right" | — |
animate | boolean | undefined | false | Отмечать ли появление и рост **счётчика** анимацией. Точке анимировать нечего. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Элемент, к которому крепится метка. |
Примеры 4
Счётчик поверх кнопок и иконок
Счётчик живёт поверх любого контрола и не требует менять его самого. max сворачивает крупные числа в «99+», showZero оставляет ноль на виду, tone и placement подбирают цвет и угол.
<script setup lang="ts">
import { GrBadgeWrap, GrButton } from '@feugene/granularity'
</script>
<template>
<div class="flex flex-wrap items-center gap-4">
<GrBadgeWrap :value="3">
<GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>
<GrBadgeWrap :value="120" :max="99" tone="primary">
<GrButton size="sm" variant="outline">Approvals</GrButton>
</GrBadgeWrap>
<GrBadgeWrap :value="0" show-zero tone="neutral" placement="bottom-right">
<GrButton size="sm" variant="outline">Drafts</GrButton>
</GrBadgeWrap>
<GrBadgeWrap :value="7" tone="success">
<GrButton size="sm">Notifications</GrButton>
</GrBadgeWrap>
</div>
</template>Точка вместо числа: «требует внимания»
Когда важно не количество, а сам факт нового события, точка работает легче. Она декоративна, пока ей не дали aria-label — тогда её слышит и скринридер.
<script setup lang="ts">
import { GrAvatar, GrBadgeWrap, GrCard } from '@feugene/granularity'
</script>
<template>
<div class="flex flex-wrap items-center gap-6">
<GrBadgeWrap dot aria-label="Unread messages">
<GrAvatar :size="40">AD</GrAvatar>
</GrBadgeWrap>
<GrBadgeWrap dot tone="warning">
<GrAvatar :size="40" shape="square">QA</GrAvatar>
</GrBadgeWrap>
<GrCard class="p-4 text-sm text-[var(--gr-muted-fg)]">
Dot mode is useful when the exact count is less important than “requires attention now”.
</GrCard>
</div>
</template>Отметка только что пришедшего
С animate счётчик отмечает «попом» то, о чём пользователь ещё не знает: своё появление и рост числа. Убыль молчит — это след его собственного действия. Пачка изменений даёт один «поп», а не пять: пока анимация играет, новое значение её не перезапускает.
<script setup lang="ts">
import { GrBadgeWrap, GrButton, GrCard } from '@feugene/granularity'
import { ref } from 'vue'
const unread = ref(3)
/** Пять писем подряд — то, на чём анимация превратилась бы в мельтешение. */
function receiveBatch() {
for (let i = 0; i < 5; i++) setTimeout(() => unread.value++, i * 40)
}
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-6">
<GrBadgeWrap :value="unread" :max="99" animate>
<GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>
<div class="flex flex-wrap items-center gap-2">
<GrButton size="sm" variant="ghost-border" @click="unread++">One message</GrButton>
<GrButton size="sm" variant="ghost-border" @click="receiveBatch">Batch of five</GrButton>
<GrButton size="sm" variant="ghost" @click="unread = 0">Mark all read</GrButton>
</div>
</div>
<GrCard class="p-4 text-sm text-[var(--gr-muted-fg)]">
The badge pops when the count appears or grows, and stays silent when it drops — a falling number is the trace of
the user's own action. A burst gives one pop, not five: while the animation plays a new value does not restart it.
</GrCard>
</div>
</template>Навигация и вкладки со счётчиком
Компонент работает через слот, поэтому счётчик вешается на вкладку, кнопку или пункт навигации без специального API на их стороне.
<script setup lang="ts">
import { GrBadgeWrap, GrButton } from '@feugene/granularity'
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrBadgeWrap :value="2">
<GrButton size="sm" variant="ghost-border">Inbox</GrButton>
</GrBadgeWrap>
<GrBadgeWrap dot>
<GrButton size="sm" variant="ghost-border">Deployments</GrButton>
</GrBadgeWrap>
<GrButton size="sm" variant="ghost-border">Audit log</GrButton>
</div>
<div class="text-sm text-[var(--gr-muted-fg)]">
Because `GrBadgeWrap` is slot-based, it can decorate buttons, tabs, icons or avatars without changing their internals.
</div>
</div>
</template>