Badge
Una pequeña etiqueta utilizada para mostrar indicadores de estado, categorías, conteos o etiquetas junto a otro contenido.
Casos de Uso
- Mostrar estado de asunto o ticket (abierto, cerrado, en progreso)
- Mostrar etiquetas de categoría o tag en artículos y publicaciones
- Indicar conteos de notificaciones en elementos de navegación o iconos
- Marcar elementos como nuevo, beta o deprecated
- Mostrar roles de usuario o niveles de permisos (admin, editor, visor)
- Destacar niveles de precios o disponibilidad de características
- Mostrar etiquetas de tecnología en tarjetas de proyectos
Implementación Más Simple
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.
Variaciones
Variantes de Color
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.
Con Indicador de Punto
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).
Removible / Descartable
"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".
Píldora vs Redondeado
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.
Con Icono
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.
Conteo de Notificación
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.
Implementación Compleja
"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:
- Tres estilos de variante --
solidpara badges de alto énfasis,softpara etiquetas sutiles por defecto, youtlinepara peso visual mínimo. Las tres están disponibles para cada color. - Mapas de color anidados -- una estructura
Record<Color, Record<Variant, string>>facilita añadir nuevos colores o variantes sin tocar la lógica condicional. - Tamaño proporcional -- punto, icono, botón de cerrar y relleno todos se escalan con el tamaño del badge para que el equilibrio visual se mantenga en cada tamaño.
- El punto se adapta a la variante -- el color del punto cambia entre claro (en fondos sólidos) y oscuro (en fondos suave/contorno) para permanecer visible contra cualquier variante.
- forwardRef -- permite adjuntar refs para posicionamiento de tooltips, bibliotecas de animación o medir dimensiones de badge en diseños dinámicos.
- Patrón de opacidad del botón de eliminar -- usa
opacity-60 hover:opacity-100en lugar de clases de color separadas, que funciona correctamente en todos los colores y combinaciones de variantes sin mapeos adicionales.
Trampa
-
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-flexdentro de un contenedor padreflexpueden no ajustarse como se espera. Envuelve múltiples badges en un contenedor conflex 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] truncatesi 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
startTransitionpara agrupar actualizaciones.