GrBadgeWrap
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 number —
dotshows a dot with no digit; - the number may be large —
maxfolds it into “99+”; - zero has to be shown —
showZero; by default the counter disappears rather than drawing “0”.
When to take something else
| Need | Take |
|---|---|
| The label sits in the flow, not on top of a control | GrBadge |
| A presence status on an avatar | GrAvatar |
| The counter is part of a tab | GrTabs with badge |
| The counter is part of a navigation section | GrBottomNav 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.
| Transition | What happens |
|---|---|
| there was no counter → it appeared | a “pop” |
| the number grew | a “pop” |
| the number went down | silently |
| the counter disappeared | silently |
| the first render of the page | silently |
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
| Prop | Values | Default |
|---|---|---|
tone | the whole GR_TONES scale (neutral, primary, success, warning, danger, info, slate, azure) | danger |
placement | top-right | top-left | bottom-right | bottom-left | top-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" />
| Variable | Default (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…
<GrBadgeWrap />Install
npm i @feugene/granularityImport
import { GrBadgeWrap } from '@feugene/granularity/components/GrBadgeWrap'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | "danger" | — |
ariaLabel | string | undefined | undefined | The label of the counter for a screen reader. If it is not set, it comes from the locale. |
value | string | number | undefined | undefined | — |
dot | boolean | undefined | false | A dot instead of a number: pure decoration unless `ariaLabel` is set. |
max | number | undefined | undefined | The threshold: values above it are drawn as "{max}+". |
showZero | boolean | undefined | false | Whether to show a zero value. By default zero is hidden. |
placement | "top-right" | "top-left" | "bottom-right" | "bottom-left" | undefined | "top-right" | — |
animate | boolean | undefined | false | Whether to mark the appearance and the growth of the **counter** with an animation. A dot has nothing to animate. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The element the label is attached to. |
Examples 4
Numeric overlays on buttons and icons
<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
<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
<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
<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>