Badge

A little context, right where it matters. Use Badge for status, counts, versions, and short labels that help readers understand the surrounding content.

Badge

View source · API reference

Badge renders an inline span with two appearance props and a default slot. Its content describes something; use Btn or BtnLink when the reader should take an action.

Playground

Edit the label and explore every tone and variant. The default surface treatment works well for supporting metadata. A stronger treatment can make a short, important status easier to find.

<RegorApp id="badge-playground" src="./badge-playground.ts"/>
import {
  type ComponentVariant,
  defineBadgeComponents,
  defineButtonComponents,
  defineFlexComponents,
  defineFormInputField,
  defineFormSelectField,
  defineGridComponents,
  defineIconComponents,
  type FormSelectOption,
} from '@purestack/ts-components'
import { SEMANTIC_TONES, type SemanticTone } from '@purestack/ts-style'
import { lucide_chevron_down } from '@purestack/ts-svg-icons'
import { batch, createApp, defineComponent, html, type Ref, ref } from 'regor'

export interface BadgePlayground {
  badgeLabel: Ref<string>
  tone: Ref<SemanticTone>
  variant: Ref<ComponentVariant>
  tones: FormSelectOption[]
  variants: FormSelectOption[]
  reset: () => void
}

const badgePlaygroundTemplate = html`<Flex direction="column">
  <Flex align="center" justify="center" class="p-4">
    <Badge :tone="tone" :variant="variant">{{ badgeLabel }}</Badge>
  </Flex>
  <Grid columns="1" columnsSm="2" columnsLg="3">
    <FormInputField id="badge-label" label="Label" :model="badgeLabel"/>
    <FormSelectField id="badge-tone" label="Tone" :model="tone" :options="tones"/>
    <FormSelectField id="badge-variant" label="Variant" :model="variant" :options="variants"/>
  </Grid>
  <Btn variant="link" @click="reset">Reset playground</Btn>
</Flex>`

function createBadgePlayground(): BadgePlayground {
  const badgeLabel = ref('Stable')
  const tone = ref<SemanticTone>('success')
  const variant = ref<ComponentVariant>('surface')
  return {
    badgeLabel,
    tone,
    variant,
    tones: SEMANTIC_TONES.map((value) => ({ label: value, value })),
    variants: [
      'solid',
      'surface',
      'surfaceAlt',
      'spotlight',
      'glass',
      'flat',
      'flatAlt',
      'flatSolid',
      'outlineFill',
      'outline',
      'subtle',
      'subtleBtn',
      'link',
      'sheen',
      'underline',
      'rail',
      'bracket',
      'none',
    ].map((value) => ({ label: value, value })),
    reset: () =>
      batch(() => {
        badgeLabel('Stable')
        tone('success')
        variant('surface')
      }),
  }
}

const component = defineComponent<BadgePlayground>(badgePlaygroundTemplate, {
  context: createBadgePlayground,
})

createApp(
  {
    components: {
      BadgePlayground: component,
      ...defineBadgeComponents(),
      ...defineButtonComponents(),
      ...defineFlexComponents(),
      ...defineGridComponents(),
      ...defineFormInputField(),
      ...defineFormSelectField(),
      ...defineIconComponents((name) => {
        if (name !== 'lucide:chevron-down')
          throw new Error(`Icon is not registered: ${name}`)
        return lucide_chevron_down
      }),
    },
  },
  { selector: 'app#badge-playground', template: html`<BadgePlayground/>` },
)

Tones

Use tone to reinforce the meaning already expressed by the label. “Passing”, “Review needed”, and “Failed” remain understandable without their colors.

neutral accent feature secondary custom ghost info success warning danger
<Flex wrap="true" align="center">
  <Badge tone="neutral">neutral</Badge>
  <Badge tone="accent">accent</Badge>
  <Badge tone="feature">feature</Badge>
  <Badge tone="secondary">secondary</Badge>
  <Badge tone="custom">custom</Badge>
  <Badge tone="ghost">ghost</Badge>
  <Badge tone="info">info</Badge>
  <Badge tone="success">success</Badge>
  <Badge tone="warning">warning</Badge>
  <Badge tone="danger">danger</Badge>
</Flex>

Variants

Badge accepts every shared variant and defaults to surface. Its variants use stateless styling: they do not add the hover and active behavior used by buttons. Interaction-oriented treatments may therefore look similar on a badge.

solid surface surfaceAlt spotlight glass flat flatAlt flatSolid outlineFill outline subtle subtleBtn link sheen underline rail bracket none
<Flex wrap="true" align="center">
  <Badge tone="accent" variant="solid">solid</Badge>
  <Badge tone="accent" variant="surface">surface</Badge>
  <Badge tone="accent" variant="surfaceAlt">surfaceAlt</Badge>
  <Badge tone="accent" variant="spotlight">spotlight</Badge>
  <Badge tone="accent" variant="glass">glass</Badge>
  <Badge tone="accent" variant="flat">flat</Badge>
  <Badge tone="accent" variant="flatAlt">flatAlt</Badge>
  <Badge tone="accent" variant="flatSolid">flatSolid</Badge>
  <Badge tone="accent" variant="outlineFill">outlineFill</Badge>
  <Badge tone="accent" variant="outline">outline</Badge>
  <Badge tone="accent" variant="subtle">subtle</Badge>
  <Badge tone="accent" variant="subtleBtn">subtleBtn</Badge>
  <Badge tone="accent" variant="link">link</Badge>
  <Badge tone="accent" variant="sheen">sheen</Badge>
  <Badge tone="accent" variant="underline">underline</Badge>
  <Badge tone="accent" variant="rail">rail</Badge>
  <Badge tone="accent" variant="bracket">bracket</Badge>
  <Badge tone="accent" variant="none">none</Badge>
</Flex>

Inline composition

The default slot accepts text and inline components. Put a badge beside a heading, give a count an explicit accessible name, or add a decorative icon next to a meaningful status label. Badge has no icon or size prop.

Public API Stable v1.0

Open reviews 3

All checks passed
<Flex direction="column" align="start">
  <Flex wrap="true" align="center">
    <strong>Public API</strong>
    <Badge tone="success">Stable</Badge>
    <Badge variant="outline">v1.0</Badge>
  </Flex>
  <p class="m-0">Open reviews <Badge tone="info" aria-label="3 open reviews">3</Badge></p>
  <Badge tone="success" variant="outline">
    <Icon name="tabler:check"/>
    All checks passed
  </Badge>
</Flex>

Live release status

Complete the three checks to enable publishing. The badge counts completed checks, switches to a success tone when ready, and becomes “Published” after you publish the preview. Reset starts a new checklist.

This example runs entirely in your browser. It models a release workflow without sending data. A named checklist, disabled action, and live status region make the changing state understandable beyond color.

<RegorApp id="release-checklist" src="./release-checklist.ts"/>
import {
  defineBadgeComponents,
  defineBtnGroupComponents,
  defineButtonComponents,
  defineFlexComponents,
  defineFormComponents,
} from '@purestack/ts-components'
import type { SemanticTone } from '@purestack/ts-style'
import {
  batch,
  type ComputedRef,
  computed,
  createApp,
  defineComponent,
  html,
  type Ref,
  ref,
} from 'regor'

export interface ReleaseChecklist {
  documentation: Ref<boolean>
  tests: Ref<boolean>
  review: Ref<boolean>
  published: Ref<boolean>
  completed: ComputedRef<number>
  status: ComputedRef<string>
  tone: ComputedRef<SemanticTone>
  message: ComputedRef<string>
  publishDisabled: ComputedRef<boolean>
  publish: () => void
  reset: () => void
}

const releaseChecklistTemplate = html`<Flex direction="column" align="start">
  <fieldset class="m-0 p-3 w-full">
    <legend>Release checklist</legend>
    <Flex direction="column" align="start">
      <FormCheck id="release-documentation" label="Documentation is up to date" :checked="documentation" :disabled="published"/>
      <FormCheck id="release-tests" label="Tests are passing" :checked="tests" :disabled="published"/>
      <FormCheck id="release-review" label="Review is complete" :checked="review" :disabled="published"/>
    </Flex>
  </fieldset>
  <Flex align="center" wrap="true" role="status">
    <Badge :tone="tone">{{ status }}</Badge>
    <span>{{ message }}</span>
  </Flex>
  <BtnGroup :wrap="true" role="group" aria-label="Release actions">
    <Btn tone="accent" :disabled="publishDisabled" @click="publish">Publish preview</Btn>
    <Btn variant="surface" @click="reset">Reset checklist</Btn>
  </BtnGroup>
</Flex>`

function createReleaseChecklist(): ReleaseChecklist {
  const documentation = ref(false)
  const tests = ref(false)
  const review = ref(false)
  const published = ref(false)
  const completed = computed(
    () => Number(documentation()) + Number(tests()) + Number(review()),
  )
  const status = computed(() =>
    published() ? 'Published' : `${completed()}/3 ready`,
  )
  const tone = computed<SemanticTone>(() =>
    published() || completed() === 3 ? 'success' : 'warning',
  )
  const message = computed<string>(() =>
    published()
      ? 'Preview release published.'
      : completed() === 3
        ? 'All checks passed. Ready to publish.'
        : 'Complete every check to publish the preview.',
  )
  const publishDisabled = computed(() => completed() !== 3 || published())
  return {
    documentation,
    tests,
    review,
    published,
    completed,
    status,
    tone,
    message,
    publishDisabled,
    publish: () => {
      if (!publishDisabled()) published(true)
    },
    reset: () =>
      batch(() => {
        documentation(false)
        tests(false)
        review(false)
        published(false)
      }),
  }
}

const component = defineComponent<ReleaseChecklist>(releaseChecklistTemplate, {
  context: createReleaseChecklist,
})

createApp(
  {
    components: {
      ReleaseChecklist: component,
      ...defineBadgeComponents(),
      ...defineBtnGroupComponents(),
      ...defineButtonComponents(),
      ...defineFlexComponents(),
      ...defineFormComponents(),
    },
  },
  { selector: 'app#release-checklist', template: html`<ReleaseChecklist/>` },
)

Accessibility

  • Include a readable label that carries the meaning. Tone alone is not a status message.
  • Badge is a non-interactive span. It has no automatic role, tab stop, or keyboard action.
  • For a changing status that needs announcing, place the badge and its explanation inside a role="status" region, as in the release example. Static metadata does not need a live region.
  • Give isolated counts enough context, through surrounding text or an aria-label such as “3 open reviews”.
  • Decorative icons should accompany a meaningful text label. PureStack's Icon component hides its SVG from assistive technology by default.
  • Keep labels short and check their contrast in the theme and variant you choose, especially when customizing semantic colors.

API reference

<Badge tone="success">Stable</Badge>

A native <span> with two appearance props and one default slot. The default variant is surface. Static badges need no browser application.

Both props accept values or Regor refs. Register defineBadgeComponents() when using Badge in a browser application. Register the icon family separately if the default slot contains an Icon.

Appearance

tone

SemanticTone
Default
inherited

Selects a semantic palette. When omitted, Badge inherits the surrounding tone, which is neutral at the theme root. The label should explain the meaning even when colors cannot be distinguished.

neutralaccentfeaturesecondarycustomghostinfosuccesswarningdanger
PassingReview neededFailedQueued
<Badge tone="success">Passing</Badge>
<Badge tone="warning">Review needed</Badge>
<Badge tone="danger">Failed</Badge>
<Badge tone="info">Queued</Badge>

variant

ComponentVariant
Default
surface

Sets the visual treatment using the stateless form of the shared variants. A variant changes appearance; it never turns the badge into a link or a button.

solidsurfacesurfaceAltspotlightglassflatflatAltflatSolidoutlineFilloutlinesubtlesubtleBtnlinksheenunderlinerailbracketnone
SolidSurfaceOutlineSubtle
<Badge tone="accent" variant="solid">Featured</Badge>
<Badge tone="success">Stable</Badge>
<Badge variant="outline">v1.0</Badge>

Badge always uses stateless variant styling. It does not expose a variantMode prop.

Slots and native attributes

Default slot

The badge's inline content: a label, a count, or text with an icon. The slot can contain reactive interpolation inside a Regor app.

Native attributes

id, class, title, and ARIA attributes reach the root span. Use ordinary attributes such as aria-label; Badge has no dedicated ariaLabel prop.

Events

Badge defines no custom events or interaction behavior. A clickable filter, removable tag, or navigation control should use an appropriate interactive component.

<Badge tone="info" aria-label="12 unread notifications">12</Badge>

Reactive content

Bind tone or variant to refs and use interpolation for the label. When appearance is derived from other state, pass a computed ref so the component keeps updating.

The release checklist includes the full implementation: typed state, computed count and tone, a guarded publish action, and a live status region. Its TypeScript source tab is the exact file mounted by the preview.

For navigation, see BtnLink. For related action rows, see BtnGroup.