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
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryMicro Interaction
ReactMotionTailwind CSS
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryMicro Interaction
ReactMotionTailwind CSS