GrBadgeWrap

Package: @feugene/granularitycoreGroup: feedback

Adds a status or count badge on top of an element.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the counter belongs to the control — unread items on a mail icon, goods in a basket, tasks on a tab;
  • the fact matters, not the numberdot shows a dot with no digit;
  • the number may be largemax folds it into “99+”;
  • zero has to be shownshowZero; by default the counter disappears rather than drawing “0”.

When to take something else

NeedTake
The label sits in the flow, not on top of a controlGrBadge
A presence status on an avatarGrAvatar
The counter is part of a tabGrTabs with badge
The counter is part of a navigation sectionGrBottomNav with badge

The counter

<GrBadgeWrap :value="3">
  <GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>

The number is drawn decoratively, with a visually hidden label beside it — otherwise a screen reader would read “Inbox, 3” without a unit, or would not read the counter at all. The text comes from the locale (gr.badgeWrap.count), and ariaLabel overrides it.

Zero is not shown by default: an empty circle on an icon reads as a defect. showZero brings it back when zero is a meaningful state (“0 drafts”).

Trimming large numbers

<GrBadgeWrap :value="120" :max="99" />

What is seen is “99+”, and what is announced is the real number: “120 unread”. “99 plus” tells the user nothing.

The function is available separately: import { formatBadgeValue } from '@feugene/granularity'.

The dot

<GrBadgeWrap dot aria-label="There are unread items" />

The dot carries no number and stays decorative until it is given an ariaLabel — then the fact of the event is announced with a word. It suits the case where “needs attention” matters and “how many exactly” does not.

Animating the counter

<GrBadgeWrap :value="unread" animate>
  <GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>

animate marks with a “pop” what the user does not know about yet: the counter appearing and the number growing. A decrease is the trace of the user’s own action, and highlighting it means flashing the badge on every letter read.

TransitionWhat happens
there was no counter → it appeareda “pop”
the number grewa “pop”
the number went downsilently
the counter disappearedsilently
the first render of the pagesilently

A string value (value="NEW") has no order, so any change of it counts as an event.

A batch of changes gives one “pop”: while the animation plays, a new value does not restart it. Five letters that arrive within a second are marked with a single movement rather than with flickering.

animate is off by default. Whether the animation is appropriate is known by the application: a counter that changes once an hour and a counter that changes constantly are different cases, and the library picks the quiet one for the consumer.

The dot (dot) takes no part in the animation: it has no value, and “growth” is not defined for it.

The movement obeys prefers-reduced-motion through the global clamp of the package — the details are in motion.md. The duration and the curve come from --gr-duration-base and --gr-ease-out, that is, they are set by the theme along with all the rest of the movement.

Tone and placement

PropValuesDefault
tonethe whole GR_TONES scale (neutral, primary, success, warning, danger, info, slate, azure)danger
placementtop-right | top-left | bottom-right | bottom-lefttop-right

The text colour comes from the paired token --gr-<tone>-fg rather than from a white literal: the badge survives a change of theme.

The offset from the corner is set with variables — by the theme or inline:

<GrBadgeWrap :value="3" style="--gr-badge-wrap-offset-x: -0.75rem" />
VariableDefault (counter / dot)
--gr-badge-wrap-offset-x-0.5rem / -0.25rem
--gr-badge-wrap-offset-y-0.5rem / -0.25rem

Why not `GrBadge` inside

The wrapper draws its own minimal badge deliberately: otherwise every counter would drag a full-size component along with it, with its own scale of sizes and radii, for the sake of a circle 20 pixels across.

Playground 7

Loading…

Code
<GrBadgeWrap />

Install

npm i @feugene/granularity

Import

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

API

Props

PropTypedefaultDescription
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"danger"
ariaLabelstring | undefinedundefinedThe label of the counter for a screen reader. If it is not set, it comes from the locale.
valuestring | number | undefinedundefined
dotboolean | undefinedfalseA dot instead of a number: pure decoration unless `ariaLabel` is set.
maxnumber | undefinedundefinedThe threshold: values above it are drawn as "{max}+".
showZeroboolean | undefinedfalseWhether to show a zero value. By default zero is hidden.
placement"top-right" | "top-left" | "bottom-right" | "bottom-left" | undefined"top-right"
animateboolean | undefinedfalseWhether to mark the appearance and the growth of the **counter** with an animation. A dot has nothing to animate.

Slots

SlotTypeDescription
defaultanyThe element the label is attached to.

Examples 4

Numeric overlays on buttons and icons

3 new120 new0 new7 new

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>

Dot mode for attention indicators

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>

Marking a count that just arrived

3 new
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>

Navigation and tab decorations

2 new
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>

Component documentationAll components