GrBadgeWrap

  • core
  • feedback
  • API · 8 props

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

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

Overview

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 — dot shows a dot with no digit;
  • the number may be large — max folds it into “99+”;
  • zero has to be shown — showZero; 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

Variants every tone and view, rendered by the real component

tone

  • primary
  • neutral
  • success
  • warning
  • danger
  • info
  • slate
  • azure

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 4 each with a live demo and its code

  • The header of an app: how many, how urgent

    The tone carries the urgency: red is left for alerts that need action today, the long queue of messages is the calm primary. max folds 128 into «99+», and aria-label keeps the real number for the screen reader. A fact without a number is a dot, and on a round avatar it sits on the circle by itself.

    Acme Billing
    128 unread messages3 alerts need actionALBilling details are missing
  • New messages and mentions, two corners of one tile

    A workspace with mentions usually has new messages too, and the rail shows both: the dot at the top says «something happened», the number at the bottom — placement="bottom-right" — says how many times you were called. Open a workspace and its marks move into the channel list, where the count stands in the line and is a GrBadge.

    Acme Billing
  • A counter that pops only for news

    animate marks what the buyer does not know yet: the counter appearing or growing. Removing an item is their own action, and the number falls quietly; at zero the counter leaves the icon altogether. The calm neutral tone suits a cart — nothing in it is an alarm.

    Northwind Office1 items in the cart
    Office chair, oak€349 · ships in 3 days
    In the cart: €349
  • Zero is the answer

    Reports saved without signal wait in the outbox. By default a zero removes the counter, but here «nothing waits to be sent» is the very thing an inspector checks before leaving the site: show-zero keeps it, and the tone turns from warning to success. Sync drains the queue without a pop; a new report brings it back.

    Bremen yardReport #2041 Outbox 2 reports waiting to be sent

    2 waiting · last sync 09:40

API generated from the package · cannot drift

Props

PropTypedefaultDescription
placement"top-right" | "top-left" | "bottom-right" | "bottom-left" | undefined"top-right"The corner the label hangs in. `top-right` by default. The offset from the corner is set by the `--gr-badge-wrap-offset-x/-y` variables.
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
"danger"The colour of the label from the tone scale: the fill is the tone role, the text is its `-fg`. `danger` by default.
ariaLabelstring | undefinedundefinedThe label of the counter for a screen reader. If it is not set, it comes from the locale.
valuestring | number | undefinedundefinedThe value of the counter: a number or a short string ("NEW"). Not set — there is no counter; zero is hidden until `showZero` is on.
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.
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.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrBadgeWrapProps, GrBadgeWrapTone, GrBadgeWrapPlacement } from '@feugene/granularity/components/GrBadgeWrap'

Filedist/types/components/GrBadgeWrap/GrBadgeWrap.vue.d.ts

GrBadgeWrapPropsinterface
interface GrBadgeWrapProps {
    value?: string | number;
    dot?: boolean;
    max?: number;
    showZero?: boolean;
    tone?: GrBadgeWrapTone;
    placement?: GrBadgeWrapPlacement;
    ariaLabel?: string;
    animate?: boolean;
}

Filedist/types/components/GrBadgeWrap/config.d.ts

grBadgeWrapConfigconst
const grBadgeWrapConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBadgeWrap">;

Filedist/types/components/GrBadgeWrap/grBadgeWrapStyles.d.ts

GrBadgeWrapTonetype
type GrBadgeWrapTone = GrTone;
GrBadgeWrapPlacementtype
type GrBadgeWrapPlacement = typeof GR_BADGE_WRAP_PLACEMENTS[number];
formatBadgeValuefunction
function formatBadgeValue(value: string | number, max?: number): string;

Filedist/types/components/GrBadgeWrap/safelist.d.ts

grBadgeWrapSafelistconst
const grBadgeWrapSafelist: string[];

Usage from the library prose

The counter
vue
<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
vue
<GrBadgeWrap :value="120" :max="99">
  <GrButton size="sm" variant="outline">Inbox</GrButton>
</GrBadgeWrap>

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
vue
<GrBadgeWrap dot aria-label="There are unread items">
  <GrAvatar :size="40">AD</GrAvatar>
</GrBadgeWrap>

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.

A child is mandatory. The badge is positioned absolutely relative to the root, and a root without content has zero size: the dot would be drawn around an empty spot, take up nothing in the layout and overlap its neighbours. If a status marker is needed on its own, in the flow — that is the dot of GrBadge.

Animating the counter
vue
<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:

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

By default the dot does not reach beyond the frame: the corner of the frame is empty on a rounded button and on a round avatar, and a dot centred on it would hang next to the control. With a zero offset it sits on the arc of the rounding or on the edge of the circle — like the status dot of GrAvatar. The counter is larger and covers the rounding anyway.

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.