Skip to main content

Halo Badge

A status badge whose color lives entirely in a glowing dot: the pill is always ink, while the dot ripples an expanding halo and casts a soft colored shadow around the badge. Variants only swap the dot and halo hue, so a cluster of badges reads calm instead of rainbow.

Micro InteractionReactMotionTailwind CSS
CSSTailwind

Manual

Create a file and paste the following code into it.

halo-badge.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
"use client";

import { type HTMLAttributes } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/cn";

/* ------------------------------------------------------------------ */
/*  HaloBadge                                                          */
/* ------------------------------------------------------------------ */

export type HaloBadgeVariant = "neutral" | "success" | "warning" | "error" | "info";

interface HaloBadgeProps extends HTMLAttributes<HTMLSpanElement> {
  /** Status color of the dot + halo — the pill itself stays ink [Optional, default: "neutral"] */
  variant?: HaloBadgeVariant;
  /** Animate the halo ripple [Optional, default: true] */
  pulse?: boolean;
  /** Show the status dot [Optional, default: true] */
  dot?: boolean;
}

/* Status lives in the dot and its halo only; the pill stays ink so a row of
   badges never turns into a rainbow. */
const VARIANTS: Record<HaloBadgeVariant, { dot: string; halo: string; ring: string }> = {
  neutral: { dot: "bg-zinc-400", halo: "bg-zinc-400", ring: "shadow-zinc-500/25" },
  success: { dot: "bg-emerald-400", halo: "bg-emerald-400", ring: "shadow-emerald-500/40" },
  warning: { dot: "bg-amber-400", halo: "bg-amber-400", ring: "shadow-amber-500/40" },
  error: { dot: "bg-red-400", halo: "bg-red-400", ring: "shadow-red-500/40" },
  info: { dot: "bg-sky-400", halo: "bg-sky-400", ring: "shadow-sky-500/40" },
};

/**
 * A status badge whose color lives entirely in a glowing dot: the pill is
 * always ink (dark solid), while the dot ripples an expanding halo and casts
 * a soft colored shadow around the badge. Variants only swap the dot + halo
 * hue, so a cluster of badges reads calm instead of rainbow.
 * @param {HaloBadgeVariant} variant - Dot + halo color [Optional, default: "neutral"]
 * @param {boolean} pulse - Ripple the halo [Optional, default: true]
 */
export function HaloBadge({
  variant = "neutral",
  pulse = true,
  dot = true,
  children,
  className,
  ...props
}: HaloBadgeProps) {
  const styles = VARIANTS[variant];
  return (
    <span className="relative inline-flex">
      <span
        className={cn(
          "relative inline-flex items-center gap-2 rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-zinc-100 shadow-[0_0_24px_-2px] dark:bg-zinc-100 dark:text-zinc-900",
          styles.ring,
          className,
        )}
        {...props}
      >
        {dot && (
          <span className="relative flex size-2 shrink-0">
            {pulse && (
              <motion.span
                className={cn("absolute inline-flex h-full w-full rounded-full opacity-75", styles.halo)}
                animate={{ scale: [1, 2.6, 1], opacity: [0.75, 0, 0.75] }}
                transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
              />
            )}
            <span className={cn("relative inline-flex size-2 rounded-full", styles.dot)} />
          </span>
        )}
        {children}
      </span>
    </span>
  );
}

Update the import paths to match your project setup.

Similar components

Tilt Pointer

Halftone Portrait

Edge Veil Blur

Spotlight Follow

Install via CLI

Resource details

PublishedJuly 17, 2026
CategoryMicro Interaction
ReactMotionTailwind CSS