EmBadge
A small pill for a count, a short label, or a bare status dot — six variants over Emerald’s status palette, with numeric capping like 99+.
Usage
EmBadge is a single <span> with three ways to fill it. content takes a number or a short string; the default slot takes arbitrary markup and wins over content when both are present; dot renders an empty 8px status dot and suppresses content entirely.
Numbers get one convenience: pass max and any numeric content strictly greater than it renders as max+ — content=120, max=99 shows 99+, while content=99 still shows 99. The cap applies to numbers only; string content is rendered as-is regardless of max. Digits are set in tabular figures, so a badge counting up does not wobble as its digits change.
There is no auto-hide: content=0 renders a badge that says 0. When zero means “nothing to show”, wrap the badge in v-if — the component does not decide that for you.
Anatomy
<script setup lang="ts">
import { EmBadge } from '@paper/emerald'
</script>
<template>
<EmBadge />
</template>Composed on v0
EmBadge renders v0’s Atom — the polymorphic primitive at the bottom of the component system — pinned to as="span". Atom’s other two modes exist but are not exposed here: EmBadge forwards neither as nor renderless, so the anatomy is fixed. A badge is always exactly one span.
What Emerald gets from the Atom base is its attribute forwarding. Everything you put on EmBadge that is not a prop — class, aria-hidden, role, an id — lands on the rendered span, which is what makes the call-site accessibility patterns below work without the component needing props for them.
The ownership split is the usual Emerald one: the component publishes state as data attributes — data-variant always, data-dot when dot is set — and the stylesheet hangs every rule off those. The capping logic (99+), the slot-over-content precedence, and the dot’s content suppression are the only behavior Emerald adds; there is no context, no namespace, and no v0 composable underneath.
Examples
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'neutral' | 'primary' | 'success' | 'danger' | 'info' | 'warning' | 'neutral' | Slot in the status palette |
content | number | string | — | Badge text when the default slot is empty. Numbers participate in max capping; strings render as-is |
max | number | — | Cap for numeric content. Strictly greater renders as max+; equal renders the number |
dot | boolean | false | Render an empty 8px status dot; suppresses content and the slot |
The default slot takes the badge’s content and wins over content when both are present. There is no namespace prop — the badge binds to nothing — and non-prop attributes fall through to the rendered span.
Accessibility
EmBadge sets no ARIA attributes of its own. It renders a span whose text participates in the accessibility tree like any other text, and everything beyond that is a call-site decision — which the attribute fallthrough is there to support.
A number needs a subject
A badge reading “3” tells a screen-reader user almost nothing on its own: three what, belonging to which control? The pattern Emerald’s own shell uses is to fold the meaning into the host’s accessible name and hide the badge from the tree:
<template>
<EmButton :aria-label="`Notifications, ${unread} unread`" variant="tertiary">
<EmIcon name="bell" size="s" />
<EmBadge aria-hidden="true" :content="unread" :max="9" variant="primary" />
</EmButton>
</template>That keeps one announcement — “Notifications, 4 unread, button” — instead of a button name followed by a floating, unexplained number. It also survives capping: the label carries the real count while the pill shows 9+.
Dots are silent
A dot badge renders an empty element — no text, no name, no role. Assistive technology skips it entirely, and so does anyone who cannot distinguish the dot’s color. Pair it with visible text, or put the state in the host’s label as the shell does for its avatar status dot. A color-only dot is not a reduced version of the state; for part of your audience it is the absence of it.
Capped counts
Once max truncates, max+ is the badge’s literal text — read out exactly as sighted users see it. If the precise number matters, surface it in the host’s label or nearby text; nothing retains it for you.