Badge
One span, three data attributes and no state at all. Every colour it can take comes from the same eight-role table Button reads, and every step it can take rides the selector unit rather than the field unit — so a theme can shrink the chips in a dense table without dragging a single control with them.
- Stable
- React 19
- Zero JS
- One DOM node
Code
<Badge>Nuevo</Badge><span class="wayra-badge" data-variant="filled">Nuevo</span>A badge marks something else: a count beside a label, a state beside a name, a version
beside a heading. It renders a <span> rather than a <div> for exactly that reason — it
has to be legal inside a paragraph, a heading or a table cell without breaking the flow
around it.
There is no dot prop and no empty form. white-space: nowrap and the inline padding are
the whole shape, so a badge with no children is a short pill and nothing else: the content
is what a badge is for.
Variants
Section titled “Variants”Code
<Badge>filled</Badge><Badge variant="outline">outline</Badge><span class="wayra-badge" data-variant="filled">filled</span><span class="wayra-badge" data-variant="outline">outline</span>filled paints the role as the background and writes the text in its -content token.
outline keeps the surface and spends the role on the text and the border instead. Those
are the only two — there is no soft and no ghost. Alert, the
component that does ship a wash, records what a role costs in contrast once it is read on
top of its own soft background; a badge sets its text at 12px, where that margin is
smallest.
Colors
Section titled “Colors”The eight roles as fills: the solid behind its own -content token.
Code
<Badge color="primary">primary</Badge><Badge color="secondary">secondary</Badge><Badge color="accent">accent</Badge><Badge color="neutral">neutral</Badge><Badge color="info">info</Badge><Badge color="success">success</Badge><Badge color="warning">warning</Badge><Badge color="error">error</Badge><span class="wayra-badge" data-variant="filled" data-color="primary">primary</span><span class="wayra-badge" data-variant="filled" data-color="secondary">secondary</span><span class="wayra-badge" data-variant="filled" data-color="accent">accent</span><span class="wayra-badge" data-variant="filled" data-color="neutral">neutral</span><span class="wayra-badge" data-variant="filled" data-color="info">info</span><span class="wayra-badge" data-variant="filled" data-color="success">success</span><span class="wayra-badge" data-variant="filled" data-color="warning">warning</span><span class="wayra-badge" data-variant="filled" data-color="error">error</span>The same eight as outlines: the solid as text on base-100. This is the row that constrains the palette.
Code
<Badge variant="outline" color="primary">primary</Badge><Badge variant="outline" color="secondary">secondary</Badge><Badge variant="outline" color="accent">accent</Badge><Badge variant="outline" color="neutral">neutral</Badge><Badge variant="outline" color="info">info</Badge><Badge variant="outline" color="success">success</Badge><Badge variant="outline" color="warning">warning</Badge><Badge variant="outline" color="error">error</Badge><span class="wayra-badge" data-variant="outline" data-color="primary">primary</span><span class="wayra-badge" data-variant="outline" data-color="secondary">secondary</span><span class="wayra-badge" data-variant="outline" data-color="accent">accent</span><span class="wayra-badge" data-variant="outline" data-color="neutral">neutral</span><span class="wayra-badge" data-variant="outline" data-color="info">info</span><span class="wayra-badge" data-variant="outline" data-color="success">success</span><span class="wayra-badge" data-variant="outline" data-color="warning">warning</span><span class="wayra-badge" data-variant="outline" data-color="error">error</span>color reaches the DOM as data-color, and styles/color.css turns that into the pair
--wayra-tone / --wayra-tone-content, which the badge reads through
--wayra-badge-color and --wayra-badge-content. That mapping is shared with Button,
Alert and every other coloured component, so one theme override moves all of them at once.
A badge with no data-color at all wears primary, because the same file declares that
default on the badge itself — and a declared value beats an inherited one, so a badge
nested inside a coloured component keeps its own default rather than picking up its
parent’s.
Code
<Badge size="xs">xs</Badge><Badge size="sm">sm</Badge><Badge size="md">md</Badge><Badge size="lg">lg</Badge><Badge size="xl">xl</Badge><span class="wayra-badge" data-variant="filled" data-size="xs">xs</span><span class="wayra-badge" data-variant="filled" data-size="sm">sm</span><span class="wayra-badge" data-variant="filled" data-size="md">md</span><span class="wayra-badge" data-variant="filled" data-size="lg">lg</span><span class="wayra-badge" data-variant="filled" data-size="xl">xl</span>| Size | Height | Inline padding | Font size |
|---|---|---|---|
xs |
18px | 7px | 10px |
sm |
20px | 8px | 11px |
md |
22px | 10px | 12px |
lg |
26px | 12px | 14px |
xl |
30px | 14px | 16px |
Measured with the default --wayra-size-selector of 0.25rem. The ladder is written as
multiples of that unit — 4.5, 5, 5.5, 6.5, 7.5 — and not of
--wayra-size-field, the unit Button and the text fields walk. The split is the point:
either group can be rescaled without dragging the other along. The default wayra theme, on :root, is
the shipped example of it, from the control side — it tightens --wayra-size-field to
0.22rem and leaves --wayra-size-selector at the default, so every button in that theme
is smaller and every badge is exactly the size it is here.
The type steps down one notch relative to Button at the same size: an md badge is on
the sm font step, an xl badge on base. It is expressed in rem rather than px, so
the badge follows the reader’s own text size (WCAG 1.4.4) instead of pinning itself at 12.
One solid, two jobs
Section titled “One solid, two jobs”This is the component that constrains the palette, and the outline row above is where it
shows. A role in this library is exactly two tokens — --wayra-color-<role> and its
-content — with no third “text” step. filled uses the solid as a background, which only
asks that the pair works against itself. outline uses the same solid as text on
base-100, which asks that it clears 4.5:1 against the page.
One colour has to pass both tests, and the two pull in opposite directions: a fill wants to
be bright, and text wants to be dark. Every role in the default theme was fitted into the
lightness window where both jobs clear AA, which is why warning there is a bronze rather
than the amber a palette reaches for first. The accessibility gate running over the outline
story is the regression test for it — a role that stopped clearing either job would fail
the build rather than ship faint.
The border is always drawn
Section titled “The border is always drawn”Both variants set a border; the base rule declares it transparent. A badge therefore
occupies the same box whichever variant it takes, and switching one in place shifts nothing
around it by a pixel.
.wayra-badge { --wayra-badge-border-color: transparent; border: var(--wayra-border) solid var(--wayra-badge-border-color);}The colour travels through a variable rather than being written as a property, and that is
load-bearing under Windows high contrast. @media adds no specificity, so a
forced-colors rescue writing border-color on .wayra-badge scores (0,1,0) and would
lose to a variant rule writing the property at (0,2,0). The variants re-point the variable
instead, which keeps the rescue reachable — the same doctrine styles/states.css follows for every
media block in the library.
Forced colors also collapses the two variants onto one look, since both become system
colours. filled swaps the system pair by hand so the distinction survives:
@media (forced-colors: active) { .wayra-badge { border-color: CanvasText; } .wayra-badge[data-variant="filled"] { background: CanvasText; color: Canvas; }}Styling contract
Section titled “Styling contract”The class plus the three attributes is the public API, and the stylesheet is written against it. Every preview on this page is hand-authored HTML with no React in it:
<span class="wayra-badge" data-variant="filled" data-color="success" data-size="md"> passing</span>data-variant is the one you cannot leave out. The base rule draws the box, the height and
the type, but neither the background nor the text colour: those live entirely on the two
variant rules, so a bare <span class="wayra-badge"> is a correctly sized pill with no
paint on it at all. data-color and data-size are safe to omit — they fall back to
primary and to the md values already on the base rule.
The class also works on an element that is not a <span>. A <a class="wayra-badge">
gets the same box, and it gets the focus ring for free: the base rule sets
--_wayra-ring-color to the badge’s own accent, which is all styles/states.css needs to
draw the shared halo on :focus-visible.
Six custom properties are yours to override, per instance or per theme:
| Property | Default | What it does |
|---|---|---|
--wayra-badge-color |
var(--wayra-tone) |
The role solid: filled background, outline text and border. |
--wayra-badge-content |
var(--wayra-tone-content) |
The text on a filled badge. |
--wayra-badge-height |
calc(var(--wayra-size-selector) * 5.5) |
Box height. The size steps rewrite it. |
--wayra-badge-padding-inline |
calc(var(--wayra-size-selector) * 2.5) |
Air on both sides of the text. |
--wayra-badge-font-size |
calc(var(--_wayra-font-size-sm) * 1rem) |
The type step. |
--wayra-badge-border-color |
transparent |
Re-pointed by both variants; the forced-colors rescue writes border-color over it. |
API reference
Section titled “API reference”BadgeProps extends ComponentProps<"span">, so id, style, ref, className,
title and every data-* land on the span. The component’s own attributes are written
after the spread, which is what makes data-color a reliable hook: a colliding
data-color passed in by a consumer loses to the color prop.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"filled" | "outline" |
"filled" |
How the role is spent: as a fill, or as text and border. |
color |
WayraColor |
"primary" |
One of the eight semantic roles. Narrows React’s legacy color attribute and is consumed as data-color rather than forwarded. |
size |
WayraSize |
"md" |
Step on the selector ladder, 18px to 30px. |
children |
ReactNode |
— | The text. A badge with none is an empty pill. |
unstyled |
boolean |
false |
Drops the wayra-badge class, keeps all three data-*. |
There is no shape, no width and no disabled. A badge is not a control: it has no
pressed, busy or unavailable state to draw, and it appears in none of the disabled rules in
styles/states.css. If something needs a state, it needs a
Button.
Accessibility
Section titled “Accessibility”- A badge is text, and it is announced. It has no
role, noaria-*and nothing hidden. Whatever is inside it is read in the flow of the sentence it sits in, which is why the examples saypassingandbetarather than showing a wordless mark. - Colour is never the only carrier. Forced colors flattens all eight roles onto
CanvasText, and greyscale flattens them for everyone else. Two badges that differ only indata-colorare the same badge to a large part of the audience — put the state in the text. - Do not name a decorative badge.
aria-labelon a<span>with no role is an axearia-prohibited-attrfailure. A badge that needs a longer name needs longer text, or text beside it. - Contrast is the two-token contract.
filledis a role on its own-content;outlineis a role onbase-100. Both are measured at 4.5:1 or better in every shipped theme butwayra, the documented exception — the one palette that trades a measured ratio for reproducing its source untouched. - Focus, if it exists, is yours. The component renders a plain
<span>, which is not focusable. The ring is defined for the class so that a badge written onto a link or given atabindexis indicated properly — but a badge you can tab to and not activate is worse than one you cannot. - Type scales. Every step is
rem-based, so a badge grows with the reader’s own text size rather than staying at a fixed 12px.
Do and don’t
Section titled “Do and don’t”Do
A word in the badge. It survives greyscale, high contrast and a screen reader.
Don't
Green and red and nothing else. Under forced-colors both become the same box, and a screen reader hears neither.
Do
If it does something, it is a : a tab stop, a key handler and a real activation.
Don't
A wearing a role it cannot honour — no tab stop, no Enter, no Space.