Skip to content

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
Nuevo passing beta 12 deprecated
Nuevo
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.

filled outline
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.

primary secondary accent neutral info success warning error

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>
primary secondary accent neutral info success warning error

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.

xs sm md lg xl
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.

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.

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; }
}

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.

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.

  • A badge is text, and it is announced. It has no role, no aria-* and nothing hidden. Whatever is inside it is read in the flow of the sentence it sits in, which is why the examples say passing and beta rather 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 in data-color are the same badge to a large part of the audience — put the state in the text.
  • Do not name a decorative badge. aria-label on a <span> with no role is an axe aria-prohibited-attr failure. A badge that needs a longer name needs longer text, or text beside it.
  • Contrast is the two-token contract. filled is a role on its own -content; outline is a role on base-100. Both are measured at 4.5:1 or better in every shipped theme but wayra, 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 a tabindex is 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

passing Deploy #482

A word in the badge. It survives greyscale, high contrast and a screen reader.

Don't

    Deploy #482     Deploy #483

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

Don't

Beta ✕

A wearing a role it cannot honour — no tab stop, no Enter, no Space.