GrBadgeWrap

Пакет: @feugene/granularityядроГруппа: Обратная связь

Берут, когда счётчик относится к контролу.

Когда брать

  • счётчик относится к контролу — непрочитанные на иконке почты, товары в корзине, задачи на вкладке;
  • важен сам факт, а не число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
placementtop-right | top-left | bottom-right | bottom-lefttop-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

PropTypeпо умолчаниюОписание
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"danger"
ariaLabelstring | undefinedundefinedПодпись счётчика для скринридера. Не задана — берётся из локали.
valuestring | number | undefinedundefined
dotboolean | undefinedfalseТочка вместо числа: чистая декорация, если не задан `ariaLabel`.
maxnumber | undefinedundefinedПорог: значения больше него рисуются как «{max}+».
showZeroboolean | undefinedfalseПоказывать ли нулевое значение. По умолчанию ноль скрыт.
placement"top-right" | "top-left" | "bottom-right" | "bottom-left" | undefined"top-right"
animateboolean | undefinedfalseОтмечать ли появление и рост **счётчика** анимацией. Точке анимировать нечего.

Slots

SlotTypeОписание
defaultanyЭлемент, к которому крепится метка.

Примеры 4

Счётчик поверх кнопок и иконок

Счётчик живёт поверх любого контрола и не требует менять его самого. max сворачивает крупные числа в «99+», showZero оставляет ноль на виду, tone и placement подбирают цвет и угол.

3 новых120 новых0 новых7 новых

Counter
<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 — тогда её слышит и скринридер.

ADUnread messagesQA
Dot mode is useful when the exact count is less important than “requires attention now”.

Dot Status
<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 счётчик отмечает «попом» то, о чём пользователь ещё не знает: своё появление и рост числа. Убыль молчит — это след его собственного действия. Пачка изменений даёт один «поп», а не пять: пока анимация играет, новое значение её не перезапускает.

3 новых
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.

Live Count
<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 на их стороне.

2 новых
Because `GrBadgeWrap` is slot-based, it can decorate buttons, tabs, icons or avatars without changing their internals.

Tab Notification
<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>

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