Primitives Layout and display
Badge
A short status or count next to other content: a <span> with a tone and a size. It is text, with no role and no state; the package adds five tones, three sizes and the rules for when a badge is read and when it is hidden. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<span>,text- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<span>
On this page
Example
<div class="row">
<span data-slean="badge">Draft</span>
<span data-slean="badge" data-variant="accent">New</span>
<span data-slean="badge" data-variant="success">Paid</span>
<span data-slean="badge" data-variant="warning">Due soon</span>
<span data-slean="badge" data-variant="danger">Overdue</span>
</div>
<div class="row">
<span data-slean="badge" data-size="sm">Small</span>
<span data-slean="badge">Medium</span>
<span data-slean="badge" data-size="lg">Large</span>
</div><script lang="ts">
import '@svelte-lean/styles/badge.css';
</script>
<div class="row">
<span data-slean="badge">Draft</span>
<span data-slean="badge" data-variant="accent">New</span>
<span data-slean="badge" data-variant="success">Paid</span>
<span data-slean="badge" data-variant="warning">Due soon</span>
<span data-slean="badge" data-variant="danger">Overdue</span>
</div>
<div class="row">
<span data-slean="badge" data-size="sm">Small</span>
<span data-slean="badge">Medium</span>
<span data-slean="badge" data-size="lg">Large</span>
</div>
<style>
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
</style>Why this implementation exists
A badge is a word or a number in a pill. The browser renders and reads text without help, so the primitive is a stylesheet and a contract: which tokens color each tone, and how a badge takes part in the name of what it labels.
The accessibility questions are the real content. A tone is color, and color alone is not information: forced-colors mode shows every tone alike, so the word has to say it. A count on an icon button is part of the button’s name, not a separate announcement.
The browser owns
- the text and its reading order
- the name of the control a count badge sits in (aria-label)
Svelte Lean owns
- badge.css: the pill, five tones from the status tokens, three sizes, tabular numbers
- the contract: the text carries the meaning, the count belongs to the control’s name
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/badge.css';Tones are data-variant="accent", "success", "warning" and "danger", with the neutral look as the default. Sizes are data-size="sm", the default and "lg".
A badge that changes while the page is open is not announced. When the change matters, write it to a live region (role="status") as well.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <span data-slean="badge"> | – | yes | data-variant="neutral|accent|success|warning|danger", data-size="sm|md|lg". Text inside. |
Runtime profile
Tier 0: the badge has no behavior module, the Vite plugin maps badge to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.
Accessibility contract
- A badge is plain text in the reading order; it has no role and is not focusable.
- The text carries the meaning; the tone repeats it, and forced colors show every tone alike with a visible border.
- Inside a control, the count is part of the control’s name (
aria-label="Inbox, 3 unread") and the badge isaria-hidden. - Every tone pair is a soft fill with its own text color from the tokens, checked by axe on the playground page.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | anywhere | Passes the badge by: it is text, not a control |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<span> and text | Widely available | Not applicable within the support policy |
Without JavaScript
Fully functional.
Server rendering
Static HTML.
Before hydration
Nothing is attached.
Styling
badge.css draws the pill, the neutral fill, the four tones from the accent and status tokens, the three sizes and tabular numbers. It positions nothing: the count recipe above is the application's CSS. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-1 | 0.25rem | gap, padding-inline |
--slean-space-2 | 0.5rem | padding-inline |
--slean-radius-full | 9999px | border-radius |
--slean-muted | var(--slean-neutral-3) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-xs | 0.75rem | font-size |
--slean-font-weight-medium | 500 | font-weight |
--slean-accent-soft | oklch(95% 0.03 258) | background |
--slean-accent-soft-fg | oklch(42% 0.17 258) | color |
--slean-success-soft | oklch(95.5% 0.04 150) | background |
--slean-success-soft-fg | oklch(40% 0.12 150) | color |
--slean-warning-soft | oklch(96% 0.05 85) | background |
--slean-warning-soft-fg | oklch(45% 0.12 75) | color |
--slean-danger-soft | oklch(95.5% 0.03 25) | background |
--slean-danger-soft-fg | oklch(45% 0.18 25) | color |
--slean-space-3 | 0.75rem | padding-inline |
--slean-text-sm | 0.875rem | font-size |
Variant attributes: data-variant (accent, success, warning, danger); data-size (sm, lg).
Compatibility notes
Everything here is widely available.
Examples
Count on an icon button
The count belongs to the button's name, so it is in the button's aria-label and the
badge is aria-hidden. Placing the badge on the corner is the application's CSS: one
declaration on the button, and on the badge its position and a ring in the page color.
<button type="button" data-slean="button" data-variant="outline" data-icon-only
aria-label="Inbox, 3 unread" class="has-count">
<svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18">…</svg>
<span data-slean="badge" data-variant="danger" data-size="sm" aria-hidden="true"
class="count">3</span>
</button>/* Application CSS: the badge package does not position anything. */
.has-count {
position: relative;
}
.count {
position: absolute;
inset-block-start: -0.375rem;
inset-inline-end: -0.375rem;
/* A ring in the page color separates the badge from the button's border. */
box-shadow: 0 0 0 2px var(--slean-bg);
}Testing
apps/playground/tests/primitives/display.spec.tsPlaywright: open state, exclusive groups, stretched links, roles and names, carousel scrolling, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/badge/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/badge/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/badge.cssthe optional stylesheet