Badge
Compact labels for status, counts, and removable tags.
Playground
<Badge>New</Badge>01 Purpose
Three props shape a badge. purpose sets what it is, intent sets the colour, and variant sets how much weight it carries: a solid filled, a quiet soft, or a light outlined.
purpose | What it renders | Example |
|---|---|---|
status (default) | A coloured label for an entity's state. | Active |
tag | A neutral label for a category. Defaults intent to neutral, because a category carries no severity. | Beta |
counter | A circular numeric pill for a count. | 8 |
dot | A contentless indicator for presence or unread state. | |
chip | An action-styled label. With an onclick it
becomes focusable and carries role="button";
without one it keeps the look and stays static. | Clickable |
removable is separate from purpose. It adds a dismiss button to any badge, so a tag or status can be
removed too (see Filter chips below).
02 Examples
Status labels
soft for ambient lists and outlined where a table needs more contrast.<Badge variant="soft" intent="success">Active</Badge>
<Badge variant="soft" intent="warning">Pending</Badge>
<Badge variant="soft" intent="danger">Suspended</Badge>
<Badge variant="outlined" intent="neutral">Draft</Badge>
<Badge variant="outlined" intent="primary">Published</Badge>Counters and dots on other components
position: relative parent with placement. Over a coloured surface, border cuts a ring in the page background so the badge stays legible.<div class="relative inline-block">
<Button variant="filled" intent="primary">Messages</Button>
<Badge intent="danger" purpose="counter" placement="top-end" border size="sm">8</Badge>
</div>
<div class="relative inline-block">
<Avatar name="Ada Lovelace" randomColor />
<Badge intent="danger" purpose="counter" placement="top-end" border size="sm">3</Badge>
</div>
<div class="relative inline-block">
<Button variant="filled" intent="neutral">Alerts</Button>
<Badge intent="success" purpose="dot" placement="top-end" border />
</div>Filter chips
removable adds a dismiss button for user-applied filters or selected tags. Pair it with onRemove.{#each filters as filter (filter)}
<Badge
variant="soft"
intent="primary"
removable
onRemove={() => removeFilter(filter)}
size="sm"
>
{filter}
</Badge>
{/each}
{#if filters.length === 0}
<span class="text-text-tertiary text-sm">No filters · </span>
<button
class="text-primary text-sm underline"
onclick={() => (filters = ['Active', 'Premium', 'Last 7 days'])}
>
reset
</button>
{/if}Inline tags
purpose=tag keeps a category label neutral inside prose, and variant=outlined keeps it light against body text.The Beta release adds support for OKLCH color tokens and a redesigned Tier API . Existing consumers can opt-in per component.
<p class="text-text-secondary text-sm leading-loose">
The <Badge purpose="tag" variant="outlined" size="sm">Beta</Badge> release adds support for
<Badge purpose="tag" variant="outlined" size="sm">OKLCH</Badge>
color tokens and a redesigned
<Badge purpose="tag" variant="outlined" size="sm">Tier API</Badge>. Existing consumers can
opt-in per component.
</p>03 Customization
Frosted glass
class gives the badge a frosted-glass look for a dark or photographic background: a translucent fill, a hairline border, and a blur behind it. It keeps the pill shape and padding.<Badge size="sm" class="border-white/20 bg-white/15 text-white backdrop-blur-md">PRO</Badge>
<Badge size="sm" class="border-white/20 bg-white/15 text-white backdrop-blur-md">Beta</Badge>
<Badge size="sm" class="border-white/20 bg-white/15 text-white backdrop-blur-md">Live</Badge>This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.
04 Accessibility
Built-in ARIA
The role follows purpose: status and dot render role="status", a polite live region that announces
content changes; tag, counter and chip carry no role — a category, a count or a filter
chip announces nothing when it changes, so it stays a plain span: a label inside a link
rather than a live region inside one. Without purpose a badge keeps role="status", and so does the deprecated counter boolean. Only a badge with an onclick is announced as a button — a chip without a handler keeps the chip look
but never claims an action it cannot perform. Removable badges include an accessible label
for the remove button. Set role="status" for a count that must be announced when
it changes, or role="alert" for a time-sensitive notification.
Keyboard
A badge with an onclick is focusable via Tab, carries role="button" and
activates on Enter / Space; without a handler it takes no tab stop. On a removable badge the
remove button takes focus, and Delete / Backspace there removes it.
05 API Reference
22 propsProp | Type | Default | Description | |
|---|---|---|---|---|
purpose required | dot | — | The canonical dot spelling — forces the pure-indicator look regardless of
variant. For the label roles use status / tag / counter / chip
(their own arm). | |
border | boolean | — | Add a ring outline in the page background color — useful to visually separate overlapping or positioned badges from their parent. | |
children | Snippet | — | Badge content (text, icons, numbers). | |
class | string | — | Extra classes merged onto the root element. | |
counter deprecated | boolean | — | Display as a compact pill for numeric counts (tightens padding, tabular-nums).
Keeps the pre-purpose ARIA default with the rest of the low-level props: a
badge driven by this boolean stays a "status" live region, where
purpose="counter" carries no role. A count that must be announced when it
changes sets role="status" explicitly. | |
disabled | boolean | — | Visually disable the badge (reduced opacity, no pointer events). | |
intent variant | dangerneutralprimarysecondary +2 more | primary | Controls the color theme and semantic meaning of the Badge. Affects the overall appearance and user perception. Available options: danger, neutral, primary, and 3 more. | |
interactive | boolean | — | Enable the interactive look (pointer cursor, hover/press scale, mint).
Automatically enabled when onclick is provided — and only the handler
makes the badge a tab stop with Enter/Space activation; interactive
alone never creates a focus stop that answers no key. | |
mint | MintProp | 'none' | Micro-interaction preset applied to the badge. Only applies while
interactive (purpose="chip", interactive, or onclick) and not
disabled. | |
onclick | (event: MouseEvent) => void | — | Click handler. Makes the badge operable: interactive styles, a tab stop, Enter/Space activation and role="button". | |
onHover | (hovered: boolean) => void | — | Called when the hover state changes. | |
onRemove | () => void | — | Fired when the remove button is clicked (only when removable is true). | |
placement | BadgePlacement | — | Anchor the badge absolutely within a position: relative parent. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Badge: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
pulse | boolean | — | Add a pulsing animation to draw attention (e.g. for live indicators). Stilled under prefers-reduced-motion. | |
removable | boolean | — | Show a remove (×) button. | |
role | statusalertbutton | — | ARIA role. Derived from purpose when unset: status and dot render
"status" — a polite live region, right for a state marker; tag,
counter and chip render no role — a category, a count or a filter chip
announces nothing when it changes, so it stays a plain span: a label
inside a link rather than a live region inside one. A badge with an
onclick handler (when not disabled) is a "button" whatever its
purpose, so assistive tech announces its activation semantics;
purpose="chip" or interactive alone change only the look — without a
handler there is nothing to activate, so no button is announced and the
badge stays outside the tab order. Without purpose a static badge keeps
"status", and so does the deprecated counter boolean. Set explicitly to
override: "status" for a count that must be announced when it changes,
"alert" for a time-sensitive notification. An explicit value always wins
over the derived default. | |
size variant | lgmdsmxs | md | Controls the dimensions, padding, and text size of the Badge. Affects the component's physical footprint. Available options: lg, md, sm, xs. | |
slotClasses | Partial<Record<BadgeSlots, string>> | — | Per-slot class overrides merged with tv styles. | |
tier variant | commitmodify | commit | Selects the semantic radius tier of the Badge — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. | |
unstyled | boolean | — | Remove all default tv classes. | |
variant | filledoutlinedsoft | 'filled' | Visual variant. dot renders a pure indicator (content hidden); the label variants accept the full surface. |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
BadgeProps | type | helper | 0 | — | |
BadgeVariants | type | variant | 0 | — | |
BadgeSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
BadgePlacement | type | helper | 1 | — | |
MintProp | type | helper | 1 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
07 Installation
Import
import { Badge } from '@urbicon-ui/blocks';