Badge
Badge is one static inline label for compact metadata: a visible status, count, plan, environment, version, or category. It renders a span, stays out of the tab order, and says nothing to assistive technology beyond its content unless the application deliberately supplies native ARIA attributes.
The Badge is never the action. When a count belongs to notifications, messages, or logs, the enclosing Button or Link owns the destination, interaction, focus, and complete accessible name.
Installation
One command detects Vue, React, or Svelte and writes the matching one-file source into the conventional component directory:
Run one command from a Boring Stack application. Klean detects the framework and conventional destination, then adds the framework-native source and its direct dependencies.
npx klean-ui add badge- No initializer or configuration file
- No framework, alias, or theme questions
- No Klean runtime dependency
There is no initializer, provider, klean-ui.json, class helper, barrel file, variant package, or runtime Klean dependency.
Usage
Write the visible meaning in the content and put the product treatment directly on Badge with Tailwind.
Vue
<script setup>
import Badge from '@/components/ui/badge/Badge.vue'
</script>
<template>
<Badge
class="bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200"
>
<span aria-hidden="true" class="size-1.5 rounded-full bg-emerald-500" />
Healthy
</Badge>
</template>
React
import Badge from '@/components/ui/badge/Badge.jsx'
export default function ServiceStatus() {
return (
<Badge className="bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Healthy
</Badge>
)
}
Svelte
<script>
import Badge from '$lib/components/ui/badge/Badge.svelte'
</script>
<Badge
class="bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200"
>
<span aria-hidden="true" class="size-1.5 rounded-full bg-emerald-500"></span>
Healthy
</Badge>
API
| Input | Default | Purpose |
|---|---|---|
class / className | — | Ordinary Tailwind classes merged after the neutral monochrome baseline. |
| native span attributes | — | IDs, titles, ARIA attributes, data attributes, event hooks, and other native attributes. |
| default content | — | Visible text and optional ordinary inline markup. |
| element reference | — | Framework-native access to the rendered span when the application genuinely needs it. |
There is no as, variant, severity, tone, status, color, size, pill, or removable API. Badge always renders one span because its contract is static inline metadata.
If the content must navigate, use a real anchor or framework Link. If it must perform work, use a real Button. Put Badge inside that control only when the metadata belongs to the control.
Notifications and counts
A notification Badge is useful, but it is still not a link or button. The parent control carries the complete accessible name, while the visual count is hidden from accessibility APIs so it is not announced twice.
When terse text appears outside a completely labelled control, add context in the Badge content:
<p>
Inbox
<Badge>3 <span class="sr-only">unread messages</span></Badge>
</p>Styling with Tailwind
The default is deliberately neutral: a compact monochrome pill with a transparent high-contrast border. Caller classes can replace every visual choice.
<Badge
class="rounded-none border-2 border-black bg-black px-3 py-1 font-mono text-[10px] font-bold text-white uppercase tracking-[0.18em]"
>
Paid
</Badge>Keep repeated status-to-class maps in the application, next to the domain values they describe. A financial product may distinguish draft, sent, and paid; an infrastructure product may distinguish healthy, deploying, and failed. Klean does not pretend those taxonomies are universal variants.
Hagfish and Slipway recipes
The same Badge can keep Hagfish expressive and Slipway operational without teaching Klean either product's status model.
Accessibility
- Use visible words such as “Healthy,” “Paid,” or “Failed.” A dot or color alone does not carry meaning.
- Badge has no
role,aria-live, or tab stop by default. Static page metadata must not announce itself like a new event. - Add
sr-onlycontext when a terse count would otherwise be ambiguous. - When Badge sits inside an already labelled Button or Link, use
aria-hidden="true"if its content is already included in the parent's accessible name. - For a non-urgent status that changes after an operation, keep the same Badge mounted with
role="status",aria-live="polite", andaria-atomic="true", then update its text. The live region must exist before the change. - Use an Alert or existing page-level status region when the message needs more context than a compact label can hold.
- Let native forced-colors mode keep a visible boundary; do not remove the caller-visible text.
Durable behavior
Badge owns no state and needs none. Counts, statuses, and labels come from server data or application state, so server rendering and the next visit reproduce the truth without a browser-only cache.
If a Badge appears inside navigation, the real anchor or Boring Stack Link owns the URL. If it appears in a notification button, the surrounding application owns unread state and the complete accessible name. Badge adds no storage, query parameter, event listener, or hydration decision.
When to use
Use Badge for compact visible metadata beside or inside richer content: invoice state, service health, environment, plan, version, unread count, category, or a short feature label.
Use it when the text remains understandable at a glance and when the inline pill treatment improves scanning.
When not to use
- Use plain text when the pill adds no useful scanning boundary.
- Use Button for a command and a real anchor or Boring Stack Link for navigation.
- Use Alert for visible guidance, warnings, failures, or results that need explanatory content.
- Use Toast for a transient application event.
- Use Tabs or Menu when a compact label is actually choosing or navigating.
- Do not use Badge as a removable tag. Tags Input owns editing, keyboard removal, focus, and form state.
Complete framework source
Copy, inspect, and change the complete one-file source for your framework.
Vue source
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'
defineOptions({ inheritAttrs: false })
const attrs = useAttrs()
const element = ref()
const forwardedAttrs = computed(() => {
const { class: _class, 'data-slot': _dataSlot, ...rest } = attrs
return rest
})
defineExpose({ element })
</script>
<template>
<span
ref="element"
v-bind="forwardedAttrs"
data-slot="badge"
:class="
twMerge(
'inline-flex items-center gap-1.5 rounded-full border border-transparent bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 text-nowrap forced-colors:border-current dark:bg-gray-800 dark:text-gray-300',
attrs.class
)
"
>
<slot />
</span>
</template>
React source
import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
const BASE_CLASSES =
'inline-flex items-center gap-1.5 rounded-full border border-transparent bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 text-nowrap forced-colors:border-current dark:bg-gray-800 dark:text-gray-300'
const Badge = forwardRef(function Badge(
{ className, 'data-slot': _dataSlot, ...props },
ref
) {
return (
<span
{...props}
ref={ref}
data-slot="badge"
className={twMerge(BASE_CLASSES, className)}
/>
)
})
export default Badge
Svelte source
<script>
import { twMerge } from "tailwind-merge";
const BASE_CLASSES =
"inline-flex items-center gap-1.5 rounded-full border border-transparent bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 text-nowrap forced-colors:border-current dark:bg-gray-800 dark:text-gray-300";
let {
children,
class: className,
"data-slot": _dataSlot,
...props
} = $props();
let element = $state();
export function getElement() {
return element;
}
</script>
<span
{...props}
bind:this={element}
data-slot="badge"
class={twMerge(BASE_CLASSES, className)}
>
{@render children?.()}
</span>
Related components
- Button — owns notification commands and other interactive Badge compositions.
- Card and Table — supply the richer content where compact metadata often appears.
- Alert — communicates guidance, results, warnings, and failures that need a real content surface.
- Toast — announces transient application events instead of turning a static Badge into a notification system.
- Tooltip — supplements a semantic control when its visible label cannot carry enough context.