HextaUI

Badge

Status labels with colored dots, removable tags that slide closed and counts that roll to their new value.

PendingIn progressIn reviewFailedSuccessExpired
import {
  IconAlertTriangle,
  IconCircleCheck,
  IconCircleX,
  IconClock,
  IconEye,
  IconLoader2,
} from "@tabler/icons-react"

import { Badge } from "@/components/ui/badge"

export function BadgeDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Badge variant="warning">
        <IconAlertTriangle data-icon="inline-start" />
        Pending
      </Badge>
      <Badge variant="info">
        <IconLoader2 data-icon="inline-start" />
        In progress
      </Badge>
      <Badge variant="warning">
        <IconEye data-icon="inline-start" />
        In review
      </Badge>
      <Badge variant="destructive">
        <IconCircleX data-icon="inline-start" />
        Failed
      </Badge>
      <Badge variant="success">
        <IconCircleCheck data-icon="inline-start" />
        Success
      </Badge>
      <Badge>
        <IconClock data-icon="inline-start" />
        Expired
      </Badge>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/badge.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  Badge,
  BadgeClose,
  BadgeCount,
  BadgeDot,
} from "@/components/ui/badge"
<Badge variant="success">
  <BadgeDot />
  Paid
</Badge>
Badge
├── BadgeDot
├── BadgeCount
└── BadgeClose

Variants and appearances

The default outline appearance keeps a neutral surface and puts the status color on the dot or icon. Use appearance="solid" when the badge needs to stand out.

DraftPaidSyncingPendingFailed
DraftPaidSyncingPendingFailed
import { Badge, BadgeDot } from "@/components/ui/badge"

export function BadgeVariants() {
  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge>
          <BadgeDot />
          Draft
        </Badge>
        <Badge variant="success">
          <BadgeDot />
          Paid
        </Badge>
        <Badge variant="info">
          <BadgeDot />
          Syncing
        </Badge>
        <Badge variant="warning">
          <BadgeDot />
          Pending
        </Badge>
        <Badge variant="destructive">
          <BadgeDot />
          Failed
        </Badge>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge appearance="solid">Draft</Badge>
        <Badge appearance="solid" variant="success">
          Paid
        </Badge>
        <Badge appearance="solid" variant="info">
          Syncing
        </Badge>
        <Badge appearance="solid" variant="warning">
          Pending
        </Badge>
        <Badge appearance="solid" variant="destructive">
          Failed
        </Badge>
      </div>
    </div>
  )
}

Sizes

sm, default and lg. Icons, dots, the close button and counts scale with the badge.

SmallPaidLivedesign42
DefaultPaidLivedesign42
LargePaidLivedesign42
import { IconCheck } from "@tabler/icons-react"

import { Badge, BadgeClose, BadgeCount, BadgeDot } from "@/components/ui/badge"

export function BadgeSizes() {
  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge size="sm">Small</Badge>
        <Badge size="sm" variant="success">
          <IconCheck data-icon="inline-start" />
          Paid
        </Badge>
        <Badge size="sm" variant="info">
          <BadgeDot pulse />
          Live
        </Badge>
        <Badge size="sm">
          design
          <BadgeClose />
        </Badge>
        <Badge size="sm">
          <BadgeCount value={42} />
        </Badge>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge>Default</Badge>
        <Badge variant="success">
          <IconCheck data-icon="inline-start" />
          Paid
        </Badge>
        <Badge variant="info">
          <BadgeDot pulse />
          Live
        </Badge>
        <Badge>
          design
          <BadgeClose />
        </Badge>
        <Badge>
          <BadgeCount value={42} />
        </Badge>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge size="lg">Large</Badge>
        <Badge size="lg" variant="success">
          <IconCheck data-icon="inline-start" />
          Paid
        </Badge>
        <Badge size="lg" variant="info">
          <BadgeDot pulse />
          Live
        </Badge>
        <Badge size="lg">
          design
          <BadgeClose />
        </Badge>
        <Badge size="lg">
          <BadgeCount value={42} />
        </Badge>
      </div>
    </div>
  )
}

Removable

Add a <BadgeClose /> to make a badge removable. It shrinks closed and its neighbours slide into the gap, then focus moves to the next close button. Remove the item from your data in onOpenChangeComplete so the exit animation can finish first.

designengineeringresearchmarketingcustomer-successops
"use client"

import * as React from "react"

import { Badge, BadgeClose } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"

const initialTags = [
  "design",
  "engineering",
  "research",
  "marketing",
  "customer-success",
  "ops",
]

export function BadgeRemovable() {
  const [tags, setTags] = React.useState(initialTags)
  const [next, setNext] = React.useState(1)

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex flex-wrap items-center gap-2">
        {tags.map((tag) => (
          <Badge
            key={tag}
            onOpenChangeComplete={(open) => {
              if (!open) {
                setTags((current) => current.filter((item) => item !== tag))
              }
            }}
          >
            {tag}
            <BadgeClose />
          </Badge>
        ))}
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => {
            setTags((current) => [...current, `tag-${next}`])
            setNext((value) => value + 1)
          }}
        >
          Add tag
        </Button>
        <Button variant="ghost" size="sm" onClick={() => setTags(initialTags)}>
          Reset
        </Button>
      </div>
    </div>
  )
}

Controlled

Pass open and onOpenChange to own the visibility. Setting open back to true brings the badge back with an enter animation.

Beta features on
"use client"

import * as React from "react"

import { Badge, BadgeClose } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"

export function BadgeControlled() {
  const [open, setOpen] = React.useState(true)

  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Badge variant="warning" open={open} onOpenChange={setOpen}>
        Beta features on
        <BadgeClose />
      </Badge>
      <Button
        variant="outline"
        size="sm"
        disabled={open}
        onClick={() => setOpen(true)}
      >
        Restore
      </Button>
    </div>
  )
}

Counts

<BadgeCount /> rolls only the digits that change, and caps at max (99 by default) with a plus sign. Screen readers always hear the real number.

99unread
"use client"

import * as React from "react"
import { IconInbox } from "@tabler/icons-react"

import { Badge, BadgeCount } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"

export function BadgeCounts() {
  const [count, setCount] = React.useState(9)

  return (
    <div className="flex flex-col items-center gap-4">
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Badge appearance="solid" variant="destructive">
          <BadgeCount value={count} />
        </Badge>
        <Badge>
          <BadgeCount value={count} max={999} />
          unread
        </Badge>
        <Button variant="outline">
          <IconInbox />
          Inbox
          <Badge size="sm">
            <BadgeCount value={count} />
          </Badge>
        </Button>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setCount((value) => value + 1)}
        >
          +1
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setCount((value) => Math.max(0, value - 1))}
        >
          −1
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setCount((value) => value + 10)}
        >
          +10
        </Button>
        <Button
          variant="ghost"
          size="sm"
          onClick={() => setCount(Math.floor(Math.random() * 1200))}
        >
          Random
        </Button>
      </div>
    </div>
  )
}

Interactive

Use render to make a badge a link or a button. It gains a hover tint, a press scale and a focus ring. aria-invalid shows the error state.

v2.0 is outNew
import { IconArrowUpRight, IconX } from "@tabler/icons-react"

import { Badge, BadgeDot } from "@/components/ui/badge"

export function BadgeInteractive() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Badge render={<a href="#changelog" />}>
        v2.0 is out
        <IconArrowUpRight data-icon="inline-end" />
      </Badge>
      <Badge render={<button type="button" />} variant="info">
        <BadgeDot />
        Filter: open
      </Badge>
      <Badge render={<a href="#new" />}>New</Badge>
      <Badge render={<button type="button" />} aria-invalid>
        <IconX data-icon="inline-start" />
        Invalid
      </Badge>
    </div>
  )
}

Long content

A badge never grows wider than its container. Long labels truncate with an ellipsis while icons and the close button stay visible.

SupercalifragilisticexpialidociousSupercalifragilisticexpialidocious[email protected]
import { IconCheck } from "@tabler/icons-react"

import { Badge, BadgeClose } from "@/components/ui/badge"

export function BadgeLongContent() {
  return (
    <div className="flex w-56 max-w-full flex-col items-start gap-2">
      <Badge>
        SupercalifragilisticexpialidociousSupercalifragilisticexpialidocious
      </Badge>
      <Badge variant="success">
        <IconCheck data-icon="inline-start" />
        [email protected]
        <BadgeClose />
      </Badge>
    </div>
  )
}

Right to left

Icons, the close button and the collapse animation follow the reading direction. Counts always read left to right.

مدفوعمباشرتصميم120
import { IconCheck } from "@tabler/icons-react"

import { Badge, BadgeClose, BadgeCount, BadgeDot } from "@/components/ui/badge"

export function BadgeRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap items-center justify-center gap-2">
      <Badge variant="success">
        <IconCheck data-icon="inline-start" />
        مدفوع
      </Badge>
      <Badge>
        <BadgeDot pulse />
        مباشر
      </Badge>
      <Badge>
        تصميم
        <BadgeClose />
      </Badge>
      <Badge>
        <BadgeCount value={120} />
      </Badge>
    </div>
  )
}

These keys apply to <BadgeClose />.

KeyAction
EnterSpaceRemoves the badge.
BackspaceDeleteRemoves the badge.
TabMoves to the next close button. After a removal, focus lands on the next close button, or the previous one when it was the last.
  • The close button is named after the badge, so a screen reader announces “Remove design” rather than a bare “Remove”. Pass aria-label to override it.
  • <BadgeDot /> is decorative and hidden from assistive tech. Keep the status in the text label.
  • <BadgeCount /> exposes the exact value even when the visible text is capped, like 99+.
  • The removal and count animations are skipped when reduced motion is on.

<Badge /> renders a <span> and accepts all of its attributes.

PropTypeDefault
variantColors the dot and icons in outline, or the surface in solid.
"default" | "success" | "info" | "warning" | "destructive""default"
appearance
"outline" | "solid""outline"
size
"sm" | "default" | "lg""default"
openControlled visibility.
boolean–
defaultOpen
booleantrue
onOpenChangeCalled when BadgeClose is activated.
(open: boolean) => void–
onOpenChangeCompleteCalled after the exit animation finishes. Remove the item from your data here.
(open: boolean) => void–
render
ReactElement | (props, state) => ReactElement<span>
AttributeDescription
data-slot="badge"Target badges in CSS.
data-variantThe current variant.
data-appearanceThe current appearance.
data-sizeThe current size.
data-ending-stylePresent while the badge animates out.
--badge-accentThe status color used by dots and icons. Override it for a custom accent.
--badge-heightThe badge height.
--badge-radiusThe corner radius. The close button derives its own radius from it.
PropTypeDefault
pulseAdds a ping animation for live states. Off when reduced motion is on.
booleanfalse
AttributeDescription
data-slot="badge-dot"Target dots in CSS.
data-pulsePresent when pulse is on.
PropTypeDefault
children
ReactNode<IconX />
aria-labelOverrides the automatic “Remove {label}” name.
string–
onClickCall event.preventDefault() to keep the badge open.
(event) => void–
AttributeDescription
data-slot="badge-close"Target the close button in CSS.
PropTypeDefault
valueNegative and non-finite values show 0.
number–
maxValues above it show as max+. Pass Infinity for no cap.
number99
duration
number600
animated
booleantrue
trendThe direction the digits spin.
"auto" | "up" | "down" | "shortest""auto"
locales
Intl.LocalesArgument–
format
Intl.NumberFormatOptions–
AttributeDescription
data-slot="badge-count"Target counts in CSS.