Badge

Displays a badge or a component that looks like a badge.

Spec · from metadata

When to use

  • Displaying status labels (active, pending, error, success)
  • Tagging or categorizing content
  • Counts or short metadata indicators
  • Color-coded labels in tables, lists, or cards

When not to use

  • Interactive chip that can be dismissed — use a custom chip component
  • Navigation tabs — use Tabs component
  • Buttons — use Button even if it looks small

Variants

PropValuesDefaultDescription
variantdefaultsecondarydestructiveoutlineghostlinkdefaultNeutraldefaultNavydefaultBerrydefaultSagedefaultOrangedefaultSkydefaultSandoutlineNeutraloutlineNavyoutlineBerryoutlineSageoutlineOrangeoutlineSkyoutlineSandsuccesswarninginfooutlineSuccessoutlineWarningoutlineInfodefaultSemantic variants (default, secondary, destructive, outline, ghost, link) for standard use. Status aliases (success, warning, info, outlineSuccess, outlineWarning, outlineInfo) render identically to defaultSage/defaultOrange/defaultNavy and their outline counterparts — use them when the badge reports a state. Primitive variants (default*/outline* + color) for brand-specific color needs, including decorative chips a status name would mislabel.

Anti-patterns

Avoid<Badge className="bg-green-500 text-white">Success</Badge>
Prefer<Badge variant="defaultSage">Success</Badge>

Never use arbitrary Tailwind colors. Use a semantic or primitive color variant.

Avoid<Badge variant="default" onClick={handleClick}>Click me</Badge>
Prefer<Button size="xs" variant="secondary">Click me</Button>

Badge is not interactive. For clickable elements, use Button.

Accessibility

  • Screen readerBadge is a <span> with no implicit role. For status indicators, consider adding role='status' or an aria-label for context.
  • ContrastAll variants are designed to meet WCAG AA contrast. Destructive uses white text on destructive background.

Token bindings

TokenCategoryUsage
bg-primarycolorDefault variant background
bg-secondarycolorSecondary variant background
bg-destructivecolorDestructive variant background
border-bordercolorOutline variant border
bg-tc-gray-900colordefaultNeutral variant background
bg-tc-berry-600colordefaultBerry variant background
bg-tc-sage-600colordefaultSage variant background
bg-tc-orange-300colordefaultOrange variant background
bg-tc-gray-100coloroutlineNeutral variant background
border-tc-gray-200coloroutlineNeutral variant border
text-tc-gray-900coloroutlineNeutral variant text
bg-tc-navy-50coloroutlineNavy variant background
border-tc-navy-200coloroutlineNavy variant border
bg-tc-berry-50coloroutlineBerry variant background
border-tc-berry-200coloroutlineBerry variant border
text-tc-berry-700coloroutlineBerry variant text — berry-700 rather than the -600 the other outline variants take from their ramps, because berry-600 on berry-50 measures 4.34:1, under the 4.5:1 text minimum
bg-tc-sage-50coloroutlineSage variant background
border-tc-sage-200coloroutlineSage variant border
text-tc-sage-600coloroutlineSage variant text
bg-tc-orange-50coloroutlineOrange variant background
border-tc-orange-200coloroutlineOrange variant border
text-tc-orange-900coloroutlineOrange variant text
bg-tc-sky-400colordefaultSky variant background — the light filled recipe (light fill + black text), chosen over sky-600 for tone separation from the navy fill: sky-600 sits ΔE76 26.8 from navy, the tightest pair in the family by more than double
bg-tc-light-sand-300colordefaultSand variant background
bg-tc-sky-50coloroutlineSky variant background
border-tc-sky-200coloroutlineSky variant border
text-tc-sky-700coloroutlineSky variant text — sky-700 rather than -600 because sky-600 on sky-50 measures 4.49:1, 0.01 under the 4.5:1 text minimum (Badge text is 12px, so the large-text allowance does not apply)
bg-tc-light-sand-50coloroutlineSand variant background
border-tc-light-sand-200coloroutlineSand variant border
text-tc-light-sand-700coloroutlineSand variant text — light-sand-700 rather than -600 because light-sand-600 on light-sand-50 measures 4.02:1, under the 4.5:1 text minimum
rounded-fullradiusPill shape border radius
Recent changes
  • new

    Badge gains sky and sand tones

    2026-08-19

  • new

    Badge takes success, warning, and info

    2026-08-19

Import

import { Badge } from "@timelycare/helix-ui"

Props

interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
  variant?:
    | "default"
    | "secondary"
    | "outline"
    | "destructive"
    | "ghost"
    | "link"
    | "defaultNeutral"
    | "defaultNavy"
    | "defaultBerry"
    | "defaultSage"
    | "defaultOrange"
    | "defaultSky"
    | "defaultSand"
    | "outlineNeutral"
    | "outlineNavy"
    | "outlineBerry"
    | "outlineSage"
    | "outlineOrange"
    | "outlineSky"
    | "outlineSand"
    | "success"
    | "warning"
    | "info"
    | "outlineSuccess"
    | "outlineWarning"
    | "outlineInfo"
  asChild?: boolean
  className?: string
  children: React.ReactNode
}

Variants

VariantUse ForBackgroundText Color
defaultPrimary actions, main categoriesbg-primarytext-primary-foreground
secondaryNeutral status, less prominent labelsbg-secondarytext-secondary-foreground
outlineSubtle indicators, bordered tagsbg-transparenttext-foreground
destructiveErrors, critical statusbg-destructivetext-destructive-foreground
ghostMinimal styling, no backgroundtransparentinherits
linkLinked badges with underline on hovertransparenttext-primary
defaultNeutralNeutral/category (filled)tc-gray-900white
defaultNavyPrimary blue (filled)primary (tc-navy-700)primary-foreground
defaultBerryBerry accent (filled)tc-berry-600white
defaultSageGreen/success (filled)tc-sage-600white
defaultOrangeOrange/warning (filled)tc-orange-300black
defaultSkySky accent (filled)tc-sky-400black
defaultSandSand accent (filled)tc-light-sand-300black
outlineNeutralNeutral outlinetc-gray-100tc-gray-900 (border: tc-gray-200)
outlineNavyNavy outlinetc-navy-50primary (border: tc-navy-200)
outlineBerryBerry outlinetc-berry-50tc-berry-700 (border: tc-berry-200)
outlineSageSage outlinetc-sage-50tc-sage-600 (border: tc-sage-200)
outlineOrangeOrange outlinetc-orange-50tc-orange-900 (border: tc-orange-200)
outlineSkySky outlinetc-sky-50tc-sky-700 (border: tc-sky-200)
outlineSandSand outlinetc-light-sand-50tc-light-sand-700 (border: tc-light-sand-200)
successSuccess status (filled) — same pixels as defaultSagetc-sage-600white
warningWarning status (filled) — same pixels as defaultOrangetc-orange-300black
infoInformational status (filled) — same pixels as defaultNavyprimary (tc-navy-700)primary-foreground
outlineSuccessSuccess status outline — same pixels as outlineSagetc-sage-50tc-sage-600 (border: tc-sage-200)
outlineWarningWarning status outline — same pixels as outlineOrangetc-orange-50tc-orange-900 (border: tc-orange-200)
outlineInfoInformational status outline — same pixels as outlineNavytc-navy-50primary (border: tc-navy-200)

Choosing a Variant

  • Default: Main categories, active status, primary labels
  • Secondary: Tags, neutral metadata, less important info
  • Outline: Minimal styling, subtle indicators, bordered tags
  • Destructive: Errors, removal actions, critical status
  • Ghost: Ultra-minimal, no background or border
  • Link: Navigational badges with underline on hover
  • success / warning / info: Filled status badges. Reach for these first when the badge reports a state — they read as the state rather than as a colour, and they match the names Alert already uses.
  • outlineSuccess / outlineWarning / outlineInfo: The same three states in the quieter outline treatment.
  • defaultNeutral / defaultNavy / defaultBerry / defaultSage / defaultOrange / defaultSky / defaultSand: Filled badges using primitive color tokens; use for categories, brand emphasis, or any decorative chip that a status name would mislabel. These colour names are permanent — a defaultNavy category chip is not info.
  • outlineNeutral / outlineNavy / outlineBerry / outlineSage / outlineOrange / outlineSky / outlineSand: Outline badges with a solid light background (50-step), colored border (200-step), and matching text; use for subtle tags or secondary emphasis.

The six status names are aliases, not new looks: each resolves to exactly the same class string as the colour variant beside it in the table above, so the two can never drift apart.

Badge's filled warning is a lighter ramp step than Alert's, and that is deliberate. Alert uses its tone as a rail (3:1, WCAG 1.4.11); Badge uses its tone as a text background (4.5:1, WCAG 1.4.3). tc-orange-300 + black measures 14.08:1; matching Alert's tc-orange-700 with white would be 4.20:1 — an AA failure at Badge's 12px text. Different bars, different steps.


Styling

Typography

PropertyValue
Fontfont-sans (Adelle Sans)
Sizetext-xs (12px)
Weightfont-semibold
Line Heightleading-4 (16px)

Dimensions

PropertyValue
Height20px
Paddingpx-2 py-0.5 (8px horizontal, 2px vertical)
Border Radiusrounded-full (pill / full rounded shape)
Icon Gapgap-1 (4px)

All badges use the full rounded (pill) shape. Do not use rounded-sm or other radii for badge components.

Color variants (primitive tokens)

Filled (default) and outline variants use Helix primitive color scales. Every colour ships in both treatments; the table below is the authority on how many there are. Anatomy:

Default (filled) — one row per color

VariantBackground tokenText token
defaultNeutraltc-gray-900white
defaultNavyprimary (tc-navy-700)primary-foreground
defaultBerrytc-berry-600white
defaultSagetc-sage-600white
defaultOrangetc-orange-300black
defaultSkytc-sky-400black
defaultSandtc-light-sand-300black

defaultSky and defaultSand join the light filled recipe (light fill + black text, as defaultOrange does) rather than the dark one. sky-600 would have been the tightest pair in the family against the navy fill (#16538E) by more than double — ΔE76 26.8, while sitting only ΔE 19.1 from sky-400 itself, i.e. nearly as close to a different tone as to another step of its own ramp. sky-400 sits ΔE 43.0 from navy.

Outline — one row per color (solid light background, 1px border, matching text)

VariantBackgroundBorderText
outlineNeutraltc-gray-100tc-gray-200tc-gray-900
outlineNavytc-navy-50tc-navy-200primary
outlineBerrytc-berry-50tc-berry-200tc-berry-700
outlineSagetc-sage-50tc-sage-200tc-sage-600
outlineOrangetc-orange-50tc-orange-200tc-orange-900
outlineSkytc-sky-50tc-sky-200tc-sky-700
outlineSandtc-light-sand-50tc-light-sand-200tc-light-sand-700

The house recipe is -50 fill / -200 border / the first text step clearing 4.5:1. For sky and sand that step is forced to -700, not chosen: sky-600 on sky-50 measures 4.49:1 (misses by 0.01) and light-sand-600 on light-sand-50 measures 4.02:1. Badge text is text-xs (12px), so the WCAG large-text allowance does not apply. Do not simplify these back to -600 — it is the same call already made for outlineBerry.


Icons

Size: size-3 (12×12px) — automatic via [&>svg]:size-3 Spacing: gap-1 (4px) — built into badge flex layout

IconUse For
CheckCompleted, approved
AlertCircleAlerts, attention needed
ArrowRightLinks, navigation
XRemovable badges
StarFeatured items

States

StateImplementation
DefaultBase appearance
HoverBuilt-in opacity/color change (anchor badges only via [a&]:hover:)
Focusring-[3px] ring-ring/50 border-ring

Common Patterns

Basic Variants

<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Destructive</Badge>

Status Badges

<Badge variant="success">Active</Badge>
<Badge variant="warning">Expiring</Badge>
<Badge variant="info">Scheduled</Badge>
<Badge variant="outlineSuccess">Verified</Badge>

With Icons

import { Check, ArrowRight } from "lucide-react"

<Badge>
  <Check />
  Approved
</Badge>

<Badge>
  Link
  <ArrowRight />
</Badge>

Notification Count (Badge Number)

// Single digit - circular
<Badge className="size-5 rounded-full p-0 justify-center">8</Badge>

// Double digit
<Badge className="h-5 min-w-5 rounded-full px-1.5 justify-center">99</Badge>

// Overflow
<Badge className="rounded-full px-2">20+</Badge>

Pending/Processing

<Badge variant="outline" className="gap-1">
  <LoaderCircle className="size-3 animate-spin" />
  Processing
</Badge>

In Context

// With card title
<div className="flex items-center gap-2">
  <CardTitle>Article Title</CardTitle>
  <Badge variant="secondary">New</Badge>
</div>

// User status
<div className="flex items-center gap-2">
  <Avatar />
  <span>Username</span>
  <Badge className="gap-1">
    <BadgeCheck />
    Verified
  </Badge>
</div>

Component Structure

{/* Standard Badge */}
<span className="inline-flex items-center justify-center gap-1 rounded-full bg-primary px-2 py-0.5 text-xs font-semibold text-primary-foreground">
  {/* Optional left icon */}
  <Check className="size-3" />
  
  {/* Badge text */}
  Badge
  
  {/* Optional right icon */}
  <ArrowRight className="size-3" />
</span>

{/* Badge Number (circular) */}
<span className="inline-flex size-5 items-center justify-center rounded-full bg-primary p-0 text-xs font-semibold text-primary-foreground">
  8
</span>

Accessibility

  • Styled inline element — no interactive behavior
  • Screen reader: reads badge text content automatically
  • Use aria-label if badge meaning isn't clear from text alone
  • No keyboard interaction (non-interactive element)
  • Use asChild to render as a different element (e.g., anchor for linked badges)

Gotchas

ProblemSolution
Badge too tallDefault height is 20px; use py-0.5 padding
Icon wrong sizeSVGs auto-size to size-3 (12px) via [&>svg]:size-3
Notification badge not circularAdd rounded-full and size-5 for single digit
Need removable badgeAdd X icon with click handler
Icons not alignedUse gap-1 for proper spacing
Need a navigational badgeUse variant="link" with asChild wrapping an anchor

See Also

  • Related Components: Avatar (avatar with status badge), Button (actionable badges)
  • Tokens: Colors — Theme-aware color tokens

Last updated: August 19, 2026