apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Badge
Variant↳ other

Data Display

Badge

Compact stylistic primitive for status labels, counts, and tag-like chips. Four variants × seven colors × three sizes × three shapes — the purest stress test of the DS variant matrix.

Overview

<Badge /> is the smallest stylistic primitive in apx-ds. Reach for it whenever a piece of UI needs a label, a count, or a status indicator — "New", "Beta", "Live", "12", "Online" — without competing with the surrounding content for attention.

Badge intentionally ships four variants (one more than Button) because count/numeric badges benefit from a subtle look that ties them to a semantic role without shouting.

Overview

Loading preview…
Overview.tsx

Variants

Pick by how much attention the label should claim on the page:

  • solid — opaque fill, contrast text. Use sparingly — status labels that need attention ("Critical", "Down").
  • outline — transparent fill, colored border + text. Tag-like UI; ghosts well on colored backgrounds.
  • soft — palette -subtle background + role-colored text. Default. The conventional "tag" look — calmly tagged, not loud.
  • subtle — neutral background + muted text + role-colored dot/icon accent. Best for counts ("12") and numeric badges that shouldn't dominate.

Every variant works with every color and every theme variant (default / tetsu / origami / katana) and adapts to the active platform. A Theme-Studio palette edit re-paints the entire 28-cell grid automatically.

Anatomy

text
[ dot | leftIcon | label | rightIcon | × ]
[ dot | leftIcon | label | rightIcon | × ]

Optional slots from left to right:

  • dot — withDot renders a 6–8px palette-colored circle before the label. Mutually exclusive with leftIcon (the dot wins). Pair with dotPulse for "Live" indicators.
  • leftIcon — any ReactNode (commonly a lucide-react icon).
  • label — the children. The accessible name when removable is set.
  • rightIcon — only renders when removable is false.
  • × (remove button) — removable swaps the right slot with a built-in keyboard-accessible <button> that fires onRemove.

Examples

Basic

Loading preview…
Basic.tsx

Variants

Loading preview…
Variants.tsx

Sizes

Loading preview…
Sizes.tsx

Colors

Loading preview…
Colors.tsx

Shapes

Loading preview…
Shapes.tsx

WithDot

Loading preview…
WithDot.tsx

WithIcons

Loading preview…
WithIcons.tsx

Removable

Loading preview…
Removable.tsx

AsChild

Loading preview…
AsChild.tsx

CountAndStatus

Loading preview…
CountAndStatus.tsx

Props

PropTypeDefaultDescription
asChildboolean—Radix-style polymorphism. When `true`, Badge merges its props/className/ref onto the single child element (e.g. wrap an `<a>` to render as a styled link/notification badge).
colorResponsiveValue<BadgeColor>'primary'Semantic palette role driving fill / text / dot colors.
dotPulseboolean—Animates the dot with a slow CSS pulse for "Live" / "Streaming" / "Active" indicators. Only applies when `withDot` is also true. Respects `prefers-reduced-motion`.
leftIconReactNode—Element rendered before the label (e.g. a `lucide-react` icon). Ignored when `withDot`.
onRemove(() => void)—Fires when the user clicks (or activates via keyboard) the built-in × button.
removableboolean—Adds a built-in `<button>` after the label that fires `onRemove` when clicked. The button is keyboard-reachable and auto-derives `aria-label="Remove {children}"` when `children` is a string; consumers can override via `removeLabel`.
removeLabelstring—Explicit aria-label for the remove button. Required when `children` isn't a plain string (e.g. when wrapping JSX) — the engine `warn` fires if it's missing in that case.
rightIconReactNode—Element rendered after the label. Ignored when `removable` (the × button takes that slot).
shapeenum'rounded'Corner radius family.
sizeResponsiveValue<BadgeSize>'md'Visual height + horizontal padding + font size.
sxSx—Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars).
variantResponsiveValue<BadgeVariant>'soft'Stylistic family.
withDotboolean—Renders a tiny status dot before the label. Decorative — always `aria-hidden`. Mutually exclusive with `leftIcon` (the dot wins when both are passed).

Accessibility

  • Renders a <span> by default — non-interactive content with no implicit role. Screen readers read the label inline with surrounding text.
  • asChild promotes the badge to whatever element the consumer passes (<a>, <button>, router <Link>) — full a11y semantics flow through via Slot.
  • withDot renders a decorative aria-hidden circle — it never appears in the accessible name.
  • dotPulse respects prefers-reduced-motion (the animation is suppressed via motion-reduce:animate-none).
  • removable renders a real <button> with an aria-label derived from children (when it's a string) or from the explicit removeLabel prop. Both Enter and Space activate it. A dev-mode warning fires when removable is set with non-string children and no removeLabel.
  • Color contrast is verified for every variant × color cell at every size — including the smallest (sm at text-[10px]).

Theming

Every visual decision is a palette token. Pull the <ThemeProvider> knobs to remix the whole library, or scope overrides to Badge specifically:

tsx
<ThemeProvider
  theme={defineTheme({
    components: {
      Badge: {
        defaultProps: { variant: 'outline', shape: 'pill' },
        styleOverrides: {
          root: 'tracking-wide uppercase',
        },
      },
    },
  })}
>
  {children}
</ThemeProvider>
<ThemeProvider
  theme={defineTheme({
    components: {
      Badge: {
        defaultProps: { variant: 'outline', shape: 'pill' },
        styleOverrides: {
          root: 'tracking-wide uppercase',
        },
      },
    },
  })}
>
  {children}
</ThemeProvider>

Instance-level overrides win via tailwind-merge:

tsx
<Badge className="font-bold" sx={{ radius: 'md' }} style={{ minWidth: 32 }}>
  3
</Badge>
<Badge className="font-bold" sx={{ radius: 'md' }} style={{ minWidth: 32 }}>
  3
</Badge>

Do / Don't

  • Do use subtle for inbox-style counts so the number doesn't dominate the nav.
  • Do pair withDot dotPulse with color="success" only when the indicator is actually live. Don't make every badge pulse — the motion stops being meaningful.
  • Don't use Badge as a button. Wrap an <a> or <button> with asChild instead so the element remains semantically interactive.
  • Don't stack more than three badges next to a single label — the eye stops grouping them after that.