Web InnoventixFreeCode

Glow Badge

Original · free

glowing new/live badges

byWeb InnoventixReact + Tailwind
badgeglowbadges
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/badge-glow.json
badge-glow.tsx
"use client";

import {
  useId,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent,
  type ReactNode,
} from "react";
import { motion, useReducedMotion } from "motion/react";

type Accent = "indigo" | "violet" | "emerald" | "rose" | "amber" | "sky";
type BadgeKind = "new" | "live" | "beta" | "pro" | "hot" | "sale";
type BadgeSize = "md" | "lg";
type CSSVars = CSSProperties & Record<`--${string}`, string>;

interface AccentMeta {
  label: string;
  rgb: string;
  dot: string;
  chip: string;
  swatch: string;
}

const ACCENTS: Record<Accent, AccentMeta> = {
  indigo: {
    label: "Indigo",
    rgb: "99 102 241",
    dot: "bg-indigo-500",
    chip: "bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-400/10 dark:text-indigo-200 dark:ring-indigo-400/25",
    swatch: "bg-indigo-500",
  },
  violet: {
    label: "Violet",
    rgb: "139 92 246",
    dot: "bg-violet-500",
    chip: "bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-400/10 dark:text-violet-200 dark:ring-violet-400/25",
    swatch: "bg-violet-500",
  },
  emerald: {
    label: "Emerald",
    rgb: "16 185 129",
    dot: "bg-emerald-500",
    chip: "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-200 dark:ring-emerald-400/25",
    swatch: "bg-emerald-500",
  },
  rose: {
    label: "Rose",
    rgb: "244 63 94",
    dot: "bg-rose-500",
    chip: "bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-400/10 dark:text-rose-200 dark:ring-rose-400/25",
    swatch: "bg-rose-500",
  },
  amber: {
    label: "Amber",
    rgb: "245 158 11",
    dot: "bg-amber-500",
    chip: "bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-200 dark:ring-amber-400/25",
    swatch: "bg-amber-500",
  },
  sky: {
    label: "Sky",
    rgb: "14 165 233",
    dot: "bg-sky-500",
    chip: "bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-400/10 dark:text-sky-200 dark:ring-sky-400/25",
    swatch: "bg-sky-500",
  },
};

const ACCENT_ORDER: readonly Accent[] = [
  "indigo",
  "violet",
  "emerald",
  "rose",
  "amber",
  "sky",
];

interface KindMeta {
  label: string;
  text: string;
  tag: string;
  live: boolean;
}

const KINDS: Record<BadgeKind, KindMeta> = {
  new: { label: "New", text: "New", tag: "Just shipped", live: false },
  live: { label: "Live", text: "Live", tag: "Streaming now", live: true },
  beta: { label: "Beta", text: "Beta", tag: "Early access", live: false },
  pro: { label: "Pro", text: "Pro", tag: "Paid plan", live: false },
  hot: { label: "Hot", text: "Hot", tag: "Trending today", live: false },
  sale: { label: "Sale", text: "Sale", tag: "40% off", live: false },
};

const KIND_ORDER: readonly BadgeKind[] = [
  "new",
  "live",
  "beta",
  "pro",
  "hot",
  "sale",
];

const GALLERY_ACCENT: Record<BadgeKind, Accent> = {
  new: "indigo",
  live: "emerald",
  beta: "violet",
  pro: "amber",
  hot: "rose",
  sale: "sky",
};

function KindGlyph({
  kind,
  className,
}: {
  kind: BadgeKind;
  className?: string;
}) {
  const base = { className, viewBox: "0 0 24 24", "aria-hidden": true } as const;
  switch (kind) {
    case "new":
      return (
        <svg {...base} fill="currentColor">
          <path d="M9.94 14.06A2 2 0 0 0 8.5 12.63l-6.14-1.58a.5.5 0 0 1 0-.97L8.5 8.5a2 2 0 0 0 1.44-1.44l1.58-6.14a.5.5 0 0 1 .96 0l1.58 6.14A2 2 0 0 0 15.5 8.5l6.14 1.58a.5.5 0 0 1 0 .97L15.5 12.63a2 2 0 0 0-1.44 1.44l-1.58 6.14a.5.5 0 0 1-.96 0z" />
        </svg>
      );
    case "pro":
      return (
        <svg {...base} fill="currentColor">
          <path d="M11.48 3.5a.6.6 0 0 1 1.07 0l2.35 4.76 5.26.77a.6.6 0 0 1 .33 1.02l-3.8 3.71.9 5.24a.6.6 0 0 1-.87.63L12 17.7l-4.7 2.47a.6.6 0 0 1-.87-.63l.9-5.24-3.8-3.71a.6.6 0 0 1 .33-1.02l5.26-.77z" />
        </svg>
      );
    case "hot":
      return (
        <svg
          {...base}
          fill="none"
          stroke="currentColor"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z" />
        </svg>
      );
    case "sale":
      return (
        <svg
          {...base}
          fill="none"
          stroke="currentColor"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M12.59 2.59A2 2 0 0 0 11.17 2H4a2 2 0 0 0-2 2v7.17a2 2 0 0 0 .59 1.41l8.7 8.7a2.43 2.43 0 0 0 3.42 0l6.58-6.58a2.43 2.43 0 0 0 0-3.42z" />
          <circle cx="7.5" cy="7.5" r="1.25" />
        </svg>
      );
    case "beta":
      return (
        <svg
          {...base}
          fill="none"
          stroke="currentColor"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M10 2v6.2a2 2 0 0 1-.28 1.02l-5.44 9.26A2 2 0 0 0 6 21.5h12a2 2 0 0 0 1.72-3.02l-5.44-9.26A2 2 0 0 1 14 8.2V2" />
          <path d="M8.5 2h7" />
          <path d="M7 15h10" />
        </svg>
      );
    default:
      return null;
  }
}

function LiveDot({
  accentDot,
  size,
  animate,
}: {
  accentDot: string;
  size: BadgeSize;
  animate: boolean;
}) {
  const s = size === "lg" ? "h-2 w-2" : "h-1.5 w-1.5";
  return (
    <span className={`relative flex ${s}`}>
      {animate ? (
        <span
          aria-hidden
          className={`bgx-live-ping absolute inline-flex h-full w-full rounded-full ${accentDot} opacity-60`}
        />
      ) : null}
      <span
        className={`relative inline-flex h-full w-full rounded-full ${accentDot}`}
      />
    </span>
  );
}

function GlowBadge({
  kind,
  accent,
  glow,
  pulse,
  intensity,
  size,
  reduced,
}: {
  kind: BadgeKind;
  accent: Accent;
  glow: boolean;
  pulse: boolean;
  intensity: number;
  size: BadgeSize;
  reduced: boolean;
}) {
  const a = ACCENTS[accent];
  const k = KINDS[kind];
  const animateGlow = glow && pulse && !reduced;
  const style: CSSVars = {};

  if (glow) {
    const alpha = (0.22 + intensity * 0.005).toFixed(3);
    const maxBlur = (8 + intensity * 0.42).toFixed(1);
    const minBlur = (5 + intensity * 0.12).toFixed(1);
    const color = `rgb(${a.rgb} / ${alpha})`;
    if (animateGlow) {
      style["--bg-gmin"] = `${minBlur}px`;
      style["--bg-gmax"] = `${maxBlur}px`;
      style["--bg-gcol"] = color;
    } else {
      style.boxShadow = `0 0 ${maxBlur}px ${color}`;
    }
  }

  const sizeCls =
    size === "lg"
      ? "px-4 py-1.5 text-sm gap-2"
      : "px-2.5 py-1 text-[11px] gap-1.5";
  const iconCls = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";

  return (
    <span
      className={`relative inline-flex items-center rounded-full font-semibold uppercase tracking-[0.09em] ring-1 ring-inset ${sizeCls} ${a.chip} ${animateGlow ? "bgx-glow-pulse" : ""}`}
      style={style}
    >
      {k.live ? (
        <LiveDot accentDot={a.dot} size={size} animate={!reduced} />
      ) : (
        <KindGlyph kind={kind} className={iconCls} />
      )}
      {k.text}
    </span>
  );
}

function OptionGroup({
  legend,
  options,
  value,
  onChange,
  columns,
  renderOption,
}: {
  legend: string;
  options: readonly string[];
  value: string;
  onChange: (v: string) => void;
  columns: string;
  renderOption: (id: string, active: boolean) => ReactNode;
}) {
  const refs = useRef<Record<string, HTMLButtonElement | null>>({});

  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    const i = options.indexOf(value);
    if (i < 0) return;
    let n = i;
    switch (e.key) {
      case "ArrowRight":
      case "ArrowDown":
        n = (i + 1) % options.length;
        break;
      case "ArrowLeft":
      case "ArrowUp":
        n = (i - 1 + options.length) % options.length;
        break;
      case "Home":
        n = 0;
        break;
      case "End":
        n = options.length - 1;
        break;
      default:
        return;
    }
    e.preventDefault();
    const id = options[n];
    onChange(id);
    refs.current[id]?.focus();
  };

  return (
    <div
      role="radiogroup"
      aria-label={legend}
      onKeyDown={onKeyDown}
      className={`grid gap-2 ${columns}`}
    >
      {options.map((id) => {
        const active = id === value;
        return (
          <button
            key={id}
            ref={(el) => {
              refs.current[id] = el;
            }}
            type="button"
            role="radio"
            aria-checked={active}
            tabIndex={active ? 0 : -1}
            onClick={() => onChange(id)}
            className="w-full rounded-xl transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
          >
            {renderOption(id, active)}
          </button>
        );
      })}
    </div>
  );
}

function Toggle({
  label,
  checked,
  onChange,
  disabled,
}: {
  label: string;
  checked: boolean;
  onChange: (v: boolean) => void;
  disabled?: boolean;
}) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      aria-label={label}
      disabled={disabled}
      onClick={() => onChange(!checked)}
      className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
        checked
          ? "bg-indigo-500"
          : "bg-slate-200 dark:bg-slate-700"
      } ${disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer"}`}
    >
      <span
        className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
          checked ? "translate-x-6" : "translate-x-1"
        }`}
      />
    </button>
  );
}

export default function BadgeGlow() {
  const reduced = useReducedMotion() ?? false;
  const [kind, setKind] = useState<BadgeKind>("live");
  const [accent, setAccent] = useState<Accent>("indigo");
  const [glow, setGlow] = useState(true);
  const [pulse, setPulse] = useState(true);
  const [intensity, setIntensity] = useState(62);

  const rangeId = useId();

  return (
    <section className="relative w-full bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes bgGlowPulse {
          0%, 100% { box-shadow: 0 0 var(--bg-gmin, 6px) var(--bg-gcol, transparent); }
          50% { box-shadow: 0 0 var(--bg-gmax, 14px) var(--bg-gcol, transparent); }
        }
        @keyframes bgLivePing {
          0% { transform: scale(1); opacity: .65; }
          75%, 100% { transform: scale(2.3); opacity: 0; }
        }
        .bgx-glow-pulse { animation: bgGlowPulse 2s ease-in-out infinite; }
        .bgx-live-ping { animation: bgLivePing 1.5s cubic-bezier(0,0,.2,1) infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bgx-glow-pulse, .bgx-live-ping { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl px-6 py-20 sm:py-24">
        <header className="max-w-2xl">
          <p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Design system · Status
          </p>
          <h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
            Badges that glow when it matters.
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Signal what&apos;s new, what&apos;s live, and what&apos;s on sale
            with status badges you can actually tune. Change the color, glow, and
            pulse below — every control works with a keyboard and screen reader.
          </p>
        </header>

        <div className="mt-12 grid items-stretch gap-6 lg:grid-cols-[1.05fr_1fr]">
          {/* Preview */}
          <div className="flex flex-col">
            <div className="relative flex min-h-[240px] flex-1 items-center justify-center overflow-hidden rounded-3xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900/50">
              <div
                aria-hidden
                className="pointer-events-none absolute inset-0"
                style={{
                  backgroundImage:
                    "radial-gradient(circle at 50% 42%, rgb(148 163 184 / 0.14), transparent 62%)",
                }}
              />
              <motion.div
                key={`${kind}-${accent}`}
                initial={reduced ? false : { scale: 0.85, opacity: 0 }}
                animate={reduced ? undefined : { scale: 1, opacity: 1 }}
                transition={{ type: "spring", stiffness: 420, damping: 26 }}
                className="relative"
              >
                <GlowBadge
                  kind={kind}
                  accent={accent}
                  glow={glow}
                  pulse={pulse}
                  intensity={intensity}
                  size="lg"
                  reduced={reduced}
                />
              </motion.div>
            </div>
            <p className="mt-4 text-center text-sm text-slate-500 dark:text-slate-400">
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                {KINDS[kind].text}
              </span>{" "}
              · {ACCENTS[accent].label} ·{" "}
              {glow
                ? `glow ${intensity}%${pulse ? " · pulsing" : ""}`
                : "no glow"}
            </p>
          </div>

          {/* Controls */}
          <div className="rounded-3xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900/50">
            <div className="space-y-6">
              <div>
                <div className="mb-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
                  Badge
                </div>
                <OptionGroup
                  legend="Badge type"
                  options={KIND_ORDER}
                  value={kind}
                  onChange={(v) => setKind(v as BadgeKind)}
                  columns="grid-cols-3"
                  renderOption={(id, active) => (
                    <span
                      className={`block rounded-xl border px-3 py-2 text-sm font-medium transition ${
                        active
                          ? "border-slate-900 bg-slate-900 text-white dark:border-white dark:bg-white dark:text-slate-900"
                          : "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600"
                      }`}
                    >
                      {KINDS[id as BadgeKind].label}
                    </span>
                  )}
                />
              </div>

              <div>
                <div className="mb-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
                  Accent
                </div>
                <OptionGroup
                  legend="Accent color"
                  options={ACCENT_ORDER}
                  value={accent}
                  onChange={(v) => setAccent(v as Accent)}
                  columns="grid-cols-6"
                  renderOption={(id, active) => {
                    const m = ACCENTS[id as Accent];
                    return (
                      <span
                        className={`flex h-10 items-center justify-center rounded-xl border transition ${
                          active
                            ? "border-slate-900 dark:border-white"
                            : "border-slate-200 dark:border-slate-700"
                        }`}
                      >
                        <span
                          className={`h-4 w-4 rounded-full ${m.swatch} ${
                            active
                              ? "ring-2 ring-slate-400/60 ring-offset-2 ring-offset-white dark:ring-offset-slate-900"
                              : ""
                          }`}
                        />
                        <span className="sr-only">{m.label}</span>
                      </span>
                    );
                  }}
                />
              </div>

              <div className="h-px bg-slate-200 dark:bg-slate-800" />

              <div className="flex items-center justify-between">
                <label
                  htmlFor="badge-glow-toggle"
                  className="text-sm font-medium text-slate-700 dark:text-slate-200"
                >
                  Glow
                  <span className="ml-2 text-xs font-normal text-slate-400 dark:text-slate-500">
                    Ambient halo
                  </span>
                </label>
                <span id="badge-glow-toggle">
                  <Toggle label="Glow" checked={glow} onChange={setGlow} />
                </span>
              </div>

              <div className="flex items-center justify-between">
                <span
                  className={`text-sm font-medium ${
                    glow
                      ? "text-slate-700 dark:text-slate-200"
                      : "text-slate-400 dark:text-slate-600"
                  }`}
                >
                  Pulse
                  <span className="ml-2 text-xs font-normal text-slate-400 dark:text-slate-500">
                    Breathing loop
                  </span>
                </span>
                <Toggle
                  label="Pulse"
                  checked={pulse}
                  onChange={setPulse}
                  disabled={!glow}
                />
              </div>

              <div>
                <div className="mb-2 flex items-center justify-between">
                  <label
                    htmlFor={rangeId}
                    className={`text-sm font-medium ${
                      glow
                        ? "text-slate-700 dark:text-slate-200"
                        : "text-slate-400 dark:text-slate-600"
                    }`}
                  >
                    Intensity
                  </label>
                  <span
                    className={`text-sm tabular-nums ${
                      glow
                        ? "text-slate-500 dark:text-slate-400"
                        : "text-slate-300 dark:text-slate-600"
                    }`}
                  >
                    {intensity}%
                  </span>
                </div>
                <input
                  id={rangeId}
                  type="range"
                  min={0}
                  max={100}
                  step={1}
                  value={intensity}
                  disabled={!glow}
                  onChange={(e) => setIntensity(Number(e.target.value))}
                  aria-label="Glow intensity"
                  className="h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
                />
              </div>
            </div>
          </div>
        </div>

        {/* Gallery */}
        <div className="mt-16">
          <div className="flex items-baseline justify-between gap-4">
            <h3 className="text-lg font-semibold tracking-tight">
              Ready-made presets
            </h3>
            <p className="text-sm text-slate-500 dark:text-slate-400">
              Six status states, drop-in.
            </p>
          </div>
          <div className="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-3">
            {KIND_ORDER.map((k) => (
              <div
                key={k}
                className="flex flex-col items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-7 dark:border-slate-800 dark:bg-slate-900/40"
              >
                <GlowBadge
                  kind={k}
                  accent={GALLERY_ACCENT[k]}
                  glow
                  pulse
                  intensity={55}
                  size="md"
                  reduced={reduced}
                />
                <span className="text-xs text-slate-500 dark:text-slate-400">
                  {KINDS[k].tag}
                </span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →