GrBadge

  • core
  • feedback
  • API · 5 props

A short label for a status, category or counter.

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

Overview

When to take it

  • the status needs a word — “Paid”, “Draft”, “Overdue”: the tone carries meaning, not only colour;
  • the label sits in the flow — in a table row, in a card, next to a heading;
  • there are many categories — the whole tone scale of the package without colours of your own;
  • the label does nothing — the component is presentational: it is not clickable and leads nowhere.

When to take something else

NeedTake
A counter on a button or an iconGrBadgeWrap
The label is dismissible or selectableGrChip; inside an input — GrInputTag
An action is neededGrButton
A number as a metricGrStatistic
A key or a shortcutGrKbd

Variants every tone and view, rendered by the real component

Variants · GrBadge
tone \ darkfalsetrue
primary
neutral
success
warning
danger
info
slate
azure

Sizes

  • xs
  • sm
  • md
  • lg

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 5 each with a live demo and its code

  • A status in one word, a category in a tag

    The tone carries the verdict and the word carries the fact: «Payment failed» in the danger tone needs no second sentence in a table row. A plan is a category rather than a verdict, so it gets radius="semi" — the shape tells the two columns apart before the colour does.

    CustomerPlanStatusMRR
    Northwind TradersEnterpriseActive€4,800
    Contoso LtdTeamTrial—
    Fabrikam IncTeamPast due€960
    Tailspin ToysStarterPayment failed€240
    Wide World ImportersEnterpriseRenewing€3,600
    LitwareStarterCancelled—
  • Quiet plates, coloured dots

    A tone name in dot paints the marker apart from the plate. Ten coloured plates in a column compete for attention; neutral plates with coloured dots read row by row, and the one red dot is found at a glance.

    • APIeu-central-1 · 99.99 % uptime
      Operational
    • Webhookseu-central-1 · 99.41 % uptime
      Degraded
    • Searchus-east-1 · 97.80 % uptime
      Partial outage
    • Exportseu-west-2 · 99.95 % uptime
      Maintenance
    • Dashboardglobal · 100 % uptime
      Operational
  • An icon tells it is still going

    The icon slot puts an icon before the label, and its wrapper takes the size from the badge step. «Processing» without a spinner reads as a final state; with animate-spin on the icon it reads as work in progress.

    • invoices-2026-09.csv2.4 MB
      Imported
    • customers.xlsx880 KB
      Processing
    • contracts.zip41 MB
      Needs review
    • payroll.csv120 KB
      Rejected
    • suppliers.csv64 KB
      Queued
  • Filled where the background is busy

    On a picture a soft plate dissolves; dark fills it with a weight that holds on any cover. On the fill the dot takes the colour of the text — the «Live» marker stays visible on red — and a semi-rounded xs badge makes a duration stamp.

    Live1:12:40

    Design tokens at scale

    Ada Lin · 3 Oct

    New48:12

    Accessible data tables

    Grace Wu · 26 Sep

    Updated36:05

    Theming without forks

    Linus Ray · 12 Sep

API generated from the package · cannot drift

Props

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe tone of the label: it carries meaning, not only colour — "Paid" in `success`, "Overdue" in `danger`. If it is not set, it comes from `GrConfigProvider`, otherwise `neutral`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedA step of the control scale `--gr-control-text-*`: a badge stands next to buttons and fields more often than inside a paragraph. If it is not set, it comes from `GrConfigProvider`.
darkboolean | undefinedfalseA fill instead of a soft backing: a dense weight for a busy background and for a label that has to stand out. The colours come from the `--gr-badge-{tone}-bg`/`-fg` layer.
radiusGrBadgeRadius | undefinedundefinedThe shape: `round` — a status pill, `semi` — a tag for a category or a code, `square` — a stamp. If it is not set, it comes from `GrConfigProvider`, otherwise `round`.
dotboolean | GrBadgeDotTone | undefinedfalseA dot marker before the label: "● Active". A tone name colours the marker separately from the plate — for the sake of the pair "quiet plate + coloured marker", which is why the pattern exists at all. `true` takes the tone of the badge itself. On a filled one (`dark`) the marker is always the colour of the text: there the tone would merge with the backing taken from the same layer.

Slots

SlotTypeDescription
defaultanyThe content of the label.
iconanyAn icon before the label: a status with a spinner, a flag, a type sign. The size is set by the wrapper from the `size` step, so the content of the slot stretches to it — `class="h-full w-full"`, as in `GrChip`. The rotation of a spinner is `animate-spin` on the icon itself. `GrProgressCircle` is not put here: a circular indicator is on the widget scale, its lowest step is `2rem`, and it does not fit into the line of a badge.

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 { GrBadgeConfigurableProps, GrBadgeTone, GrBadgeDotTone } from '@feugene/granularity/components/GrBadge'

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

grBadgeConfigconst
const grBadgeConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBadge">;

Filedist/types/components/GrBadge/defaults.d.ts

GrBadgeConfigurablePropsinterface
interface GrBadgeConfigurableProps {
    tone: GrBadgeTone;
    size: GrBadgeSize;
    radius: GrBadgeRadius;
}

Filedist/types/components/GrBadge/grBadgeStyles.d.ts

GrBadgeTonetype
type GrBadgeTone = GrTone;
GrBadgeDotTonetype
type GrBadgeDotTone = 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'slate' | 'azure';
GrBadgeSizetype
type GrBadgeSize = GrComponentSize;
GrBadgeRadiustype
type GrBadgeRadius = 'square' | 'semi' | 'round';

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

grBadgeSafelistconst
const grBadgeSafelist: string[];

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

GrBadgePropsinterface
interface GrBadgeProps {
    tone?: GrBadgeTone;
    dark?: boolean;
    size?: GrBadgeSize;
    radius?: GrBadgeRadius;
    dot?: boolean | GrBadgeDotTone;
}

Usage from the library prose

Two weights: tinted and filled

dark switches the soft tinted background for a solid fill.

The fill goes through the per-component layer --gr-badge-{tone}-bg/-fg rather than straight from the tone role, because the weight the themes need is different. In the light theme --gr-{tone} is a bright fill under dark text, and a filled badge read as a heavy, almost black plate; there the layer moves it to -solid/-solid-fg — a button-weight fill under light text, the same one solid buttons use. In the dark theme a pastel fill with dark text is the regular convention of the package, and the layer leaves the roles as they are.

The values of the layer live in the themes of the component (GrBadge/themes/{light,dark}.css), so a badge can be recoloured without touching the tone roles as a whole.

Why text on a tint is always -text

A saturated tone as the text colour on its own light background gives 2.24:1; the -text role gives 6.78:1. That is why the soft weight colours text with -text roles only.

Both guarantees are held by the grBadgeContrast.test.ts gate, and neither is formal:

  • text contrast — the AA threshold of 4.5:1. There is no large-text concession here: a badge is set at 11–14px;
  • the visibility of the plate itself is measured as the perceptual distance to the page background (ΔE, noticeability threshold 2.3). A contrast rule alone is not enough for this: the 3:1 of WCAG 1.4.11 is not met even by the regular --gr-muted/--gr-brd pair, and a badge could end up indistinguishable from the page while formally breaking nothing.

On tone roles and their suffixes — ../theming.md.

Radius

radius is round (the default), semi or square. semi has its own ladder across the size steps — 3/3/5/7 px through --gr-badge-semi-radius-*: the badge rounds more slowly than it grows, otherwise at lg it would turn into a pill sooner than the layout wants it to.

vue
<GrBadge radius="semi" size="lg">
SKU-14920
</GrBadge>
The text is centred optically

The height of the badge is held by an inner wrapper (min-height: 1lh — the height of the line box at leading-none), while the text itself is trimmed to cap height (text-box-trim) and centred inside that height. The label then sits level regardless of whether it has descenders: “Paid” and “Awaiting payment” sit the same.

The trimming lives on a nested node rather than on the text itself, and that is not a matter of style: text-box-trim shrinks the line box, and hung directly on the text it would drag the height of the whole badge down with it.

The dot marker: a quiet plate, a coloured status
vue
<GrBadge tone="neutral" dot="success">
  Active
</GrBadge>

<GrBadge tone="success" dot>
  Paid
</GrBadge>

dot puts a circle before the label. A tone name colours the marker separately from the plate — the pattern exists for the sake of that pair: in a list of a dozen rows coloured plates compete for attention, while a grey plate with a coloured dot reads row by row. A boolean value takes the tone of the badge itself.

The marker is decorative (aria-hidden): the meaning is carried by the word beside it. There is no dot without a label here on purpose — it would report the state by colour alone. A label on top of someone else’s control is GrBadgeWrap, a status on an avatar is GrAvatar.

Why the marker is coloured by the badge layer rather than by the tone role

The tone role taken directly does not work in either look, and both branches are measured by the gate:

What was triedLight themeDark theme
--gr-{tone}2.06–2.56 — fails on every surfacepasses
--gr-{tone}-solidpassesdown to 1.37 — fails
--gr-badge-{tone}-bgpassespasses

The reason is that --gr-{tone} in the light theme is a bright fill under dark text rather than a colour for small graphics. The per-component layer --gr-badge-{tone}-bg means precisely “the saturated weight that reads in this theme”, and on it all 128 pairs of “marker tone × badge tone × theme” pass.

The threshold for the marker is 3:1 (WCAG 1.4.11), not 4.5:1: the circle is recognised by shape and position, not by letterform.

On a fill (dark) the marker is always the colour of the text. The surface of a filled badge is taken from the same layer, so a marker of a named tone would merge with it into a single colour; a named tone is ignored there with a warning in dev.

The marker and the icon slot occupy the same place, so they exclude each other: if both are passed, the icon is drawn — it carries more than a colour does.

An icon before the label — the icon slot

A status often consists of more than a word: “parsing” without a spinner reads as a final state rather than as work in progress.

vue
<GrBadge tone="warning">
  <template #icon>
    <IconLoader class="h-full w-full animate-spin" />
  </template>
  Recognising
</GrBadge>

The size is held by the slot wrapper, and the content stretches to it (h-full w-full) — the same device as in GrChip, and for the same reason: the icon scale has to follow the size of the badge rather than the markup at the call site. It grows more slowly than the type: at the lower steps an icon matched one-to-one with the text would blow out a narrow plate.

The icon is a sibling of the trimmed text, not its content: text-box-trim lives on the nested node of the label and does not reach the icon, so the icon does not stretch the height of the plate.

Do not put GrProgressCircle here: a circular indicator is on the widget scale, its lowest step is 2rem, and it does not fit into the line of a badge — the arc runs outside the background and pushes the text out. A spinner here is an ordinary icon with animate-spin.

The slot does not make the badge interactive: it is still a label. If you need a dismissible or selectable tag, that is GrChip, with its own role and its own keyboard.

Size

size takes its type size from the control scale --gr-control-text-* rather than the content one: a badge more often stands in a row with buttons and fields than inside a paragraph. The scales and the difference between them — ../sizes.md.

tone, size and radius are read from GrConfigProvider, so the look of badges is set once for the whole application.