React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

badgelabelstatustagcomponenttailwind

Badge

A small label used to display status indicators, categories, counts, or tags alongside other content.

Use Cases

  • Show issue or ticket status (open, closed, in progress)
  • Display category or tag labels on blog posts and articles
  • Indicate notification counts on nav items or icons
  • Mark items as new, beta, or deprecated
  • Show user roles or permission levels (admin, editor, viewer)
  • Highlight pricing tiers or feature availability
  • Display technology tags on project cards

Simplest Implementation

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

A static badge that does not need "use client" since it has no interactivity. The inline-flex with items-center ensures the content is vertically centered even when mixed with icons.

Variations

Color Variants

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

Maps color names to Tailwind class pairs. Using the 100 background with the 800 text shade ensures readable contrast across all variants.

With Dot Indicator

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

A small colored dot before the label adds a visual cue that reinforces the badge's meaning. Useful for status badges where color alone carries semantic weight (e.g., green = active, red = error).

Removable / Dismissible

"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={`Remove ${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>
  );
}

The close button is inside the badge with asymmetric padding (pl-2.5 pr-1) to keep the layout balanced. The aria-label includes the badge text so screen readers announce "Remove TypeScript" rather than just "Remove".

Pill vs Rounded

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

Three shape options for different visual contexts. Pills feel organic and work well for tags, rounded rectangles suit table status labels, and square badges fit structured data-heavy layouts.

With Icon

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>
  );
}
 
// Usage
<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>
  }
>
  Verified
</Badge>

Icons are wrapped in a fixed-size container with shrink-0 to prevent them from compressing when the badge text is long. The gap-1 provides consistent spacing between icon and text.

Notification Count

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

A standalone count badge for notification indicators. Returns null when count is zero. The min-w-5 keeps the badge circular for single digits while px-1.5 adds horizontal padding for wider numbers.

Complex Implementation

"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={`Remove ${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>
  );
});

Key aspects:

  • Three variant styles -- solid for high-emphasis badges, soft for default subtle labels, and outline for minimal visual weight. All three are available for every color.
  • Nested color maps -- a Record<Color, Record<Variant, string>> structure makes it easy to add new colors or variants without touching conditional logic.
  • Proportional sizing -- dot, icon, close button, and padding all scale with the badge size so the visual balance is maintained at every size.
  • Dot adapts to variant -- the dot color shifts between light (on solid backgrounds) and dark (on soft/outline backgrounds) so it remains visible against any variant.
  • forwardRef -- enables attaching refs for tooltip positioning, animation libraries, or measuring badge dimensions in dynamic layouts.
  • Remove button opacity pattern -- uses opacity-60 hover:opacity-100 instead of separate color classes, which works correctly across all color and variant combinations without additional mappings.

Gotchas

  • Tailwind purge removes dynamic classes -- Constructing class names dynamically like `bg-${color}-100` does not work with Tailwind's purge. Always use complete class strings in a static map.

  • Badge inside flex wrapping oddly -- Badges using inline-flex inside a parent flex container may not wrap as expected. Wrap multiple badges in a container with flex flex-wrap gap-2.

  • Color alone conveying meaning -- Relying only on red = error and green = success excludes colorblind users. Pair colors with icons, dots, or descriptive text.

  • Remove button click area too small -- A 12px close button is hard to tap on mobile. Ensure the close button is at least 24px on touch devices, or add invisible padding with before: pseudo-element.

  • Badge text overflow -- Very long text inside a badge breaks the layout. Use max-w-[200px] truncate if badge content can vary in length.

  • Notification count flickering -- Updating a count badge rapidly (e.g., from a WebSocket) causes visual flicker. Debounce the count or use startTransition to batch updates.

  • Avatar -- Avatars often display badges for notification counts
  • Button -- Badges used inside or alongside buttons
  • Skeleton -- Loading placeholders for badge content