HextaUI

Badge

Etiquetas de estado con puntos de color, tags eliminables que se deslizan al cerrarse y contadores que ruedan hasta su nuevo valor.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

Variantes y apariencias

La apariencia outline por defecto mantiene una superficie neutra y pone el color de estado en el punto o el icono. Usa appearance="solid" cuando la insignia deba destacar, o appearance="muted" para un chip relleno discreto. shape="pill" la redondea por completo.

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 className="flex flex-wrap items-center justify-center gap-2">
        <Badge appearance="muted">Draft</Badge>
        <Badge appearance="muted" shape="pill">
          nngroup.com
        </Badge>
        <Badge appearance="muted" shape="pill" variant="info">
          <BadgeDot />
          Syncing
        </Badge>
      </div>
    </div>
  )
}

Tamaños

sm, default y lg. Los iconos, puntos, el botón de cierre y los contadores escalan con la insignia.

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>
  )
}

Removible

Añade un <BadgeClose /> para que una insignia se pueda quitar. Se encoge hasta cerrarse y sus vecinas se deslizan hacia el hueco; luego el foco pasa al siguiente botón de cierre. Quita el elemento de tus datos en onOpenChangeComplete para que la animación de salida termine antes.

"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>
  )
}

Controlado

Pasa open y onOpenChange para controlar la visibilidad tú mismo. Volver a poner open en true recupera la insignia con una animación de entrada.

"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>
  )
}

Contadores

<BadgeCount /> hace rodar solo los dígitos que cambian y se limita a max (99 por defecto) con un signo más. Los lectores de pantalla siempre oyen el número real.

"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>
  )
}

Interactiva

Usa render para convertir una insignia en un enlace o un botón. Gana un tinte al pasar el cursor, una escala al pulsar y un anillo de foco. aria-invalid muestra el estado de error.

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>
  )
}

Contenido largo

Una insignia nunca crece más que su contenedor. Las etiquetas largas se truncan con puntos suspensivos mientras los iconos y el botón de cierre permanecen visibles.

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>
  )
}

De derecha a izquierda

Los iconos, el botón de cierre y la animación de colapso siguen la dirección de lectura. Los contadores siempre se leen de izquierda a derecha.

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>
  )
}

Estas teclas se aplican a <BadgeClose />.

KeyAcción
EnterSpaceQuita la insignia.
BackspaceDeleteQuita la insignia.
TabPasa al siguiente botón de cierre. Tras una eliminación, el foco va al siguiente botón de cierre, o al anterior si era el último.
  • El botón de cierre toma el nombre de la insignia, así que un lector de pantalla anuncia “Remove design” en lugar de un simple “Remove”. Pasa aria-label para reemplazarlo.
  • <BadgeDot /> es decorativo y está oculto para las tecnologías de asistencia. Mantén el estado en la etiqueta de texto.
  • <BadgeCount /> expone el valor exacto incluso cuando el texto visible está limitado, como 99+.
  • Las animaciones de eliminación y de contador se omiten con movimiento reducido.

<Badge /> renderiza un <span> y acepta todos sus atributos.

PropTipoPredeterminado
variantColorea el punto y los iconos en outline, o la superficie en solid.
"default" | "success" | "info" | "warning" | "destructive""default"
appearance
"outline" | "solid" | "muted""outline"
shape
"default" | "pill""default"
size
"sm" | "default" | "lg""default"
openVisibilidad controlada.
boolean–
defaultOpen
booleantrue
onOpenChangeSe llama cuando se activa BadgeClose.
(open: boolean) => void–
onOpenChangeCompleteSe llama cuando termina la animación de salida. Quita aquí el elemento de tus datos.
(open: boolean) => void–
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="badge"Selecciona las insignias en CSS.
data-variantLa variante actual.
data-appearanceLa apariencia actual.
data-shapeLa forma actual.
data-sizeEl tamaño actual.
data-ending-stylePresente mientras la insignia se anima al salir.
--badge-accentEl color de estado usado por los puntos y los iconos. Reemplázalo para un acento personalizado.
--badge-heightLa altura de la insignia.
--badge-radiusEl radio de las esquinas. El botón de cierre deriva su propio radio de él.
PropTipoPredeterminado
pulseAñade una animación ping para estados en vivo. Se desactiva con movimiento reducido.
booleanfalse
AtributoDescripción
data-slot="badge-dot"Selecciona los puntos en CSS.
data-pulsePresente cuando pulse está activado.
PropTipoPredeterminado
children
ReactNode<IconX />
aria-labelReemplaza el nombre automático “Remove {label}”.
string–
onClickLlama a event.preventDefault() para mantener la insignia abierta.
(event) => void–
AtributoDescripción
data-slot="badge-close"Selecciona el botón de cierre en CSS.
PropTipoPredeterminado
valueLos valores negativos y no finitos muestran 0.
number–
maxLos valores por encima se muestran como max+. Pasa Infinity para no tener límite.
number99
duration
number600
animated
booleantrue
trendLa dirección en que giran los dígitos.
"auto" | "up" | "down" | "shortest""auto"
locales
Intl.LocalesArgument–
format
Intl.NumberFormatOptions–
AtributoDescripción
data-slot="badge-count"Selecciona los contadores en CSS.

Usado en bloques

Bloques que se construyen sobre Badge.