Badge
Una pequeña etiqueta utilizada para mostrar indicadores de estado, categorías, conteos o etiquetas junto a otro contenido.
Busca en todas las páginas de la documentación
Una pequeña etiqueta utilizada para mostrar indicadores de estado, categorías, conteos o etiquetas junto a otro contenido.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
interface BadgeProps {
children: React.ReactNode;
}
export function Badge({ children }: BadgeProps) {
return (
<span className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
{children}
</span>
);
}Un badge estático que no necesita "use client" ya que no tiene interactividad. El inline-flex con items-center asegura que el contenido esté centrado verticalmente incluso cuando se mezcla con iconos.
type BadgeColor = "gray" | "red" | "yellow" | "green" | "blue" | "purple";
interface BadgeProps {
children: React.ReactNode;
color?: BadgeColor;
}
const colorClasses: Record<BadgeColor, string> = {
gray: "bg-gray-100 text-gray-800",
red: "bg-red-100 text-red-800",
yellow: "bg-yellow-100 text-yellow-800",
green: "bg-green-100 text-green-800",
blue: "bg-blue-100 text-blue-800",
purple: "bg-purple-100 text-purple-800",
};
export function Badge({ children, color = "gray" }: BadgeProps) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClasses[color]}`}
>
{children}
</span>
);
}Asigna nombres de colores a pares de clases Tailwind. Usar el fondo 100 con el tono de texto 800 asegura contraste legible en todas las variantes.
type BadgeColor = "gray" | "red" | "green" | "blue";
interface BadgeProps {
children: React.ReactNode;
color?: BadgeColor;
}
const colorClasses: Record<BadgeColor, { bg: string; dot: string }> = {
gray: { bg: "bg-gray-100 text-gray-800", dot: "bg-gray-500" },
red: { bg: "bg-red-100 text-red-800", dot: "bg-red-500" },
green: { bg: "bg-green-100 text-green-800", dot: "bg-green-500" },
blue: { bg: "bg-blue-100 text-blue-800", dot: "bg-blue-500" },
};
export function Badge({ children, color = "gray" }: BadgeProps) {
const classes = colorClasses[color];
return (
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${classes.bg}`}>
<span className={`h-1.5 w-1.5 rounded-full ${classes.dot}`} />
{children}
</span>
);
}Un punto de color pequeño antes de la etiqueta añade una pista visual que refuerza el significado del badge. Útil para badges de estado donde el color solo tiene peso semántico (p. ej., verde = activo, rojo = error).
"use client";
interface BadgeProps {
children: React.ReactNode;
onRemove: () => void;
}
export function Badge({ children, onRemove }: BadgeProps) {
return (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 py-0.5 pl-2.5 pr-1 text-xs font-medium text-blue-800">
{children}
<button
type="button"
onClick={onRemove}
aria-label={`Eliminar ${children}`}
className="inline-flex h-4 w-4 items-center justify-center rounded-full text-blue-600 hover:bg-blue-200 hover:text-blue-800"
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
);
}El botón de cerrar está dentro del badge con relleno asimétrico (pl-2.5 pr-1) para mantener el diseño equilibrado. El aria-label incluye el texto del badge para que los lectores de pantalla anuncien "Eliminar TypeScript" en lugar de solo "Eliminar".
type BadgeShape = "pill" | "rounded" | "square";
interface BadgeProps {
children: React.ReactNode;
shape?: BadgeShape;
}
const shapeClasses: Record<BadgeShape, string> = {
pill: "rounded-full",
rounded: "rounded-md",
square: "rounded-none",
};
export function Badge({ children, shape = "pill" }: BadgeProps) {
return (
<span
className={`inline-flex items-center bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 ${shapeClasses[shape]}`}
>
{children}
</span>
);
}Tres opciones de forma para diferentes contextos visuales. Las píldoras se sienten orgánicas y funcionan bien para tags, los rectángulos redondeados son adecuados para etiquetas de estado de tabla, y los badges cuadrados se ajustan a diseños estructurados con mucho contenido.
interface BadgeProps {
children: React.ReactNode;
icon?: React.ReactNode;
}
export function Badge({ children, icon }: BadgeProps) {
return (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
{icon && <span className="h-3.5 w-3.5 shrink-0">{icon}</span>}
{children}
</span>
);
}
// Uso
<Badge
icon={
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
}
>
Verificado
</Badge>Los iconos se envuelven en un contenedor de tamaño fijo con shrink-0 para evitar que se compriman cuando el texto del badge es largo. El gap-1 proporciona espaciado consistente entre icono y texto.
interface CountBadgeProps {
count: number;
max?: number;
}
export function CountBadge({ count, max = 99 }: CountBadgeProps) {
if (count <= 0) return null;
const display = count > max ? `${max}+` : String(count);
return (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1.5 text-[11px] font-bold text-white">
{display}
</span>
);
}Un badge de conteo independiente para indicadores de notificación. Devuelve null cuando el conteo es cero. El min-w-5 mantiene el badge circular para dígitos únicos mientras px-1.5 añade relleno horizontal para números más anchos.
"use client";
import { forwardRef } from "react";
type BadgeVariant = "solid" | "soft" | "outline";
type BadgeColor = "gray" | "red" | "yellow" | "green" | "blue" | "purple";
type BadgeSize = "sm" | "md" | "lg";
interface BadgeProps {
children: React.ReactNode;
variant?: BadgeVariant;
color?: BadgeColor;
size?: BadgeSize;
dot?: boolean;
icon?: React.ReactNode;
onRemove?: () => void;
className?: string;
}
const colorMap: Record<BadgeColor, Record<BadgeVariant, string>> = {
gray: {
solid: "bg-gray-600 text-white",
soft: "bg-gray-100 text-gray-800",
outline: "border border-gray-300 text-gray-700",
},
red: {
solid: "bg-red-600 text-white",
soft: "bg-red-100 text-red-800",
outline: "border border-red-300 text-red-700",
},
yellow: {
solid: "bg-yellow-500 text-white",
soft: "bg-yellow-100 text-yellow-800",
outline: "border border-yellow-300 text-yellow-700",
},
green: {
solid: "bg-green-600 text-white",
soft: "bg-green-100 text-green-800",
outline: "border border-green-300 text-green-700",
},
blue: {
solid: "bg-blue-600 text-white",
soft: "bg-blue-100 text-blue-800",
outline: "border border-blue-300 text-blue-700",
},
purple: {
solid: "bg-purple-600 text-white",
soft: "bg-purple-100 text-purple-800",
outline: "border border-purple-300 text-purple-700",
},
};
const dotColorMap: Record<BadgeColor, Record<BadgeVariant, string>> = {
gray: { solid: "bg-gray-300", soft: "bg-gray-500", outline: "bg-gray-500" },
red: { solid: "bg-red-300", soft: "bg-red-500", outline: "bg-red-500" },
yellow: { solid: "bg-yellow-300", soft: "bg-yellow-500", outline: "bg-yellow-500" },
green: { solid: "bg-green-300", soft: "bg-green-500", outline: "bg-green-500" },
blue: { solid: "bg-blue-300", soft: "bg-blue-500", outline: "bg-blue-500" },
purple: { solid: "bg-purple-300", soft: "bg-purple-500", outline: "bg-purple-500" },
};
const sizeClasses: Record<BadgeSize, { badge: string; icon: string; dot: string; close: string }> = {
sm: { badge: "px-2 py-px text-[10px] gap-1", icon: "h-3 w-3", dot: "h-1 w-1", close: "h-3 w-3" },
md: { badge: "px-2.5 py-0.5 text-xs gap-1.5", icon: "h-3.5 w-3.5", dot: "h-1.5 w-1.5", close: "h-3.5 w-3.5" },
lg: { badge: "px-3 py-1 text-sm gap-1.5", icon: "h-4 w-4", dot: "h-2 w-2", close: "h-4 w-4" },
};
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
{
children,
variant = "soft",
color = "gray",
size = "md",
dot = false,
icon,
onRemove,
className,
},
ref
) {
const sizes = sizeClasses[size];
const colors = colorMap[color][variant];
const dotColor = dotColorMap[color][variant];
return (
<span
ref={ref}
className={[
"inline-flex items-center rounded-full font-medium",
sizes.badge,
colors,
onRemove ? "pr-1" : "",
className ?? "",
]
.filter(Boolean)
.join(" ")}
>
{dot && <span className={`shrink-0 rounded-full ${sizes.dot} ${dotColor}`} />}
{!dot && icon && <span className={`shrink-0 ${sizes.icon}`}>{icon}</span>}
{children}
{onRemove && (
<button
type="button"
onClick={onRemove}
aria-label={`Eliminar ${typeof children === "string" ? children : ""}`}
className={`inline-flex shrink-0 items-center justify-center rounded-full opacity-60 hover:opacity-100 ${sizes.close}`}
>
<svg className="h-full w-full" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</span>
);
});Aspectos clave:
solid para badges de alto énfasis, soft para etiquetas sutiles por defecto, y outline para peso visual mínimo. Las tres están disponibles para cada color.Record<Color, Record<Variant, string>> facilita añadir nuevos colores o variantes sin tocar la lógica condicional.opacity-60 hover:opacity-100 en lugar de clases de color separadas, que funciona correctamente en todos los colores y combinaciones de variantes sin mapeos adicionales.Tailwind purge elimina clases dinámicas -- Construir nombres de clase dinámicamente como `bg-${color}-100` no funciona con purge de Tailwind. Siempre usa cadenas de clase completas en un mapa estático.
Badge dentro de flex que se ajusta de forma extraña -- Los badges usando inline-flex dentro de un contenedor padre flex pueden no ajustarse como se espera. Envuelve múltiples badges en un contenedor con flex flex-wrap gap-2.
El color solo que comunica significado -- Confiar solo en rojo = error y verde = éxito excluye a usuarios daltónicos. Empareja colores con iconos, puntos o texto descriptivo.
Área de clic del botón de eliminar muy pequeña -- Un botón de cerrar de 12px es difícil de tocar en dispositivos móviles. Asegúrate de que el botón de cerrar sea de al menos 24px en dispositivos táctiles, o añade relleno invisible con pseudo-elemento before:.
Desbordamiento de texto del badge -- Texto muy largo dentro de un badge rompe el diseño. Usa max-w-[200px] truncate si el contenido del badge puede variar en longitud.
Parpadeo de conteo de notificación -- Actualizar un badge de conteo rápidamente (p. ej., desde un WebSocket) causa parpadeo visual. Debounce el conteo o usa startTransition para agrupar actualizaciones.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥