GrBadge
A short label for a status, category or counter.
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
| Need | Take |
|---|---|
| A counter on a button or an icon | GrBadgeWrap |
| The label is dismissible or selectable | GrChip; inside an input — GrInputTag |
| An action is needed | GrButton |
| A number as a metric | GrStatistic |
| A key or a shortcut | GrKbd |
Variants every tone and view, rendered by the real component
tone \ dark | false | true |
|---|---|---|
primary | ||
neutral | ||
success | ||
warning | ||
danger | ||
info | ||
slate | ||
azure |
Sizes
xssmmdlg
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrBadge>Label</GrBadge>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.Customer Plan Status MRR Northwind Traders Enterprise Active €4,800 Contoso Ltd Team Trial — Fabrikam Inc Team Past due €960 Tailspin Toys Starter Payment failed €240 Wide World Importers Enterprise Renewing €3,600 Litware Starter Cancelled — Quiet plates, coloured dots
A tone name in
dotpaints 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 % uptimeOperational
- Webhookseu-central-1 · 99.41 % uptimeDegraded
- Searchus-east-1 · 97.80 % uptimePartial outage
- Exportseu-west-2 · 99.95 % uptimeMaintenance
- Dashboardglobal · 100 % uptimeOperational
An icon tells it is still going
The
iconslot 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; withanimate-spinon the icon it reads as work in progress.- invoices-2026-09.csv2.4 MBImported
- customers.xlsx880 KBProcessing
- contracts.zip41 MBNeeds review
- payroll.csv120 KBRejected
- suppliers.csv64 KBQueued
Filled where the background is busy
On a picture a soft plate dissolves;
darkfills 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-roundedxsbadge makes a duration stamp.Live1:12:40Design tokens at scale
Ada Lin · 3 Oct
New48:12Accessible data tables
Grace Wu · 26 Sep
Updated36:05Theming without forks
Linus Ray · 12 Sep
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone |
| undefined | The 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" | undefined | undefined | A 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`. |
dark | boolean | undefined | false | A 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. |
radius | GrBadgeRadius | undefined | undefined | The 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`. |
dot | boolean | GrBadgeDotTone | undefined | false | A 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
| Slot | Type | Description |
|---|---|---|
default | any | The content of the label. |
icon | any | An 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 type { GrBadgeConfigurableProps, GrBadgeTone, GrBadgeDotTone } from '@feugene/granularity/components/GrBadge'Filedist/types/components/GrBadge/config.d.ts
const grBadgeConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBadge">;Filedist/types/components/GrBadge/defaults.d.ts
interface GrBadgeConfigurableProps {
tone: GrBadgeTone;
size: GrBadgeSize;
radius: GrBadgeRadius;
}Filedist/types/components/GrBadge/grBadgeStyles.d.ts
type GrBadgeTone = GrTone;type GrBadgeDotTone = 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'slate' | 'azure';type GrBadgeSize = GrComponentSize;type GrBadgeRadius = 'square' | 'semi' | 'round';Filedist/types/components/GrBadge/safelist.d.ts
const grBadgeSafelist: string[];Filedist/types/components/GrBadge/GrBadge.vue.d.ts
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-brdpair, 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.
<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
<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 tried | Light theme | Dark theme |
|---|---|---|
--gr-{tone} | 2.06–2.56 — fails on every surface | passes |
--gr-{tone}-solid | passes | down to 1.37 — fails |
--gr-badge-{tone}-bg | passes | passes |
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.
<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.
Props in this example