Web InnoventixFreeCode

Glow Ring Button

Original · free

Buttons with a pulsing glow ring on hover.

byWeb InnoventixReact + Tailwind
btnglowringbuttons
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/btn-glow-ring.json
btn-glow-ring.tsx
"use client";

import { useState, useRef } from "react";
import type { ReactNode, ButtonHTMLAttributes } from "react";
import { useReducedMotion } from "motion/react";

type Accent = "indigo" | "violet" | "emerald" | "rose" | "amber" | "sky";

const ACCENTS: Record<
  Accent,
  {
    ring: string;
    face: string;
    faceHover: string;
    text: string;
    glow: string;
    softFace: string;
    softText: string;
    softBorder: string;
  }
> = {
  indigo: {
    ring: "bgr-ring-indigo",
    face: "bg-indigo-600",
    faceHover: "group-hover:bg-indigo-500",
    text: "text-white",
    glow: "shadow-indigo-500/50",
    softFace: "bg-indigo-50 dark:bg-indigo-500/10",
    softText: "text-indigo-700 dark:text-indigo-300",
    softBorder: "border-indigo-200 dark:border-indigo-500/30",
  },
  violet: {
    ring: "bgr-ring-violet",
    face: "bg-violet-600",
    faceHover: "group-hover:bg-violet-500",
    text: "text-white",
    glow: "shadow-violet-500/50",
    softFace: "bg-violet-50 dark:bg-violet-500/10",
    softText: "text-violet-700 dark:text-violet-300",
    softBorder: "border-violet-200 dark:border-violet-500/30",
  },
  emerald: {
    ring: "bgr-ring-emerald",
    face: "bg-emerald-600",
    faceHover: "group-hover:bg-emerald-500",
    text: "text-white",
    glow: "shadow-emerald-500/50",
    softFace: "bg-emerald-50 dark:bg-emerald-500/10",
    softText: "text-emerald-700 dark:text-emerald-300",
    softBorder: "border-emerald-200 dark:border-emerald-500/30",
  },
  rose: {
    ring: "bgr-ring-rose",
    face: "bg-rose-600",
    faceHover: "group-hover:bg-rose-500",
    text: "text-white",
    glow: "shadow-rose-500/50",
    softFace: "bg-rose-50 dark:bg-rose-500/10",
    softText: "text-rose-700 dark:text-rose-300",
    softBorder: "border-rose-200 dark:border-rose-500/30",
  },
  amber: {
    ring: "bgr-ring-amber",
    face: "bg-amber-500",
    faceHover: "group-hover:bg-amber-400",
    text: "text-amber-950",
    glow: "shadow-amber-400/50",
    softFace: "bg-amber-50 dark:bg-amber-500/10",
    softText: "text-amber-700 dark:text-amber-300",
    softBorder: "border-amber-200 dark:border-amber-500/30",
  },
  sky: {
    ring: "bgr-ring-sky",
    face: "bg-sky-600",
    faceHover: "group-hover:bg-sky-500",
    text: "text-white",
    glow: "shadow-sky-500/50",
    softFace: "bg-sky-50 dark:bg-sky-500/10",
    softText: "text-sky-700 dark:text-sky-300",
    softBorder: "border-sky-200 dark:border-sky-500/30",
  },
};

type Size = "sm" | "md" | "lg";

const SIZES: Record<Size, string> = {
  sm: "h-9 px-4 text-sm gap-1.5",
  md: "h-11 px-6 text-sm gap-2",
  lg: "h-14 px-8 text-base gap-2.5",
};

const FOCUS =
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-900 dark:focus-visible:ring-slate-100 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";

function GlowButton({
  children,
  accent = "indigo",
  size = "md",
  disabled = false,
  loading = false,
  className = "",
  ...rest
}: {
  children: ReactNode;
  accent?: Accent;
  size?: Size;
  disabled?: boolean;
  loading?: boolean;
  className?: string;
} & ButtonHTMLAttributes<HTMLButtonElement>) {
  const a = ACCENTS[accent];
  const isOff = disabled || loading;
  return (
    <button
      type="button"
      disabled={isOff}
      aria-busy={loading || undefined}
      className={`group relative inline-flex select-none items-center justify-center rounded-full font-semibold ${SIZES[size]} ${a.face} ${a.text} ${FOCUS} transition-[transform,background-color,box-shadow] duration-200 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 ${
        isOff ? "" : `${a.faceHover} hover:shadow-lg hover:${a.glow}`
      } ${className}`}
      {...rest}
    >
      {!isOff && (
        <span
          aria-hidden="true"
          className={`pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100 ${a.ring}`}
        />
      )}
      <span className="relative z-10 inline-flex items-center justify-center gap-[inherit]">
        {loading && (
          <svg
            className="bgr-spin h-4 w-4"
            viewBox="0 0 24 24"
            fill="none"
            aria-hidden="true"
          >
            <circle
              cx="12"
              cy="12"
              r="9"
              stroke="currentColor"
              strokeWidth="3"
              className="opacity-25"
            />
            <path
              d="M21 12a9 9 0 0 0-9-9"
              stroke="currentColor"
              strokeWidth="3"
              strokeLinecap="round"
            />
          </svg>
        )}
        {children}
      </span>
    </button>
  );
}

function ArrowIcon() {
  return (
    <svg
      className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5"
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
    >
      <path
        d="M5 12h14M13 6l6 6-6 6"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export default function BtnGlowRing() {
  const reduce = useReducedMotion();
  const [liked, setLiked] = useState(false);
  const [likes, setLikes] = useState(248);
  const [subscribed, setSubscribed] = useState(false);
  const [copied, setCopied] = useState(false);
  const [loading, setLoading] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  function toggleLike() {
    setLiked((v) => {
      setLikes((n) => (v ? n - 1 : n + 1));
      return !v;
    });
  }

  async function copy() {
    try {
      await navigator.clipboard.writeText("npm i motion");
    } catch {
      /* clipboard unavailable */
    }
    setCopied(true);
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => setCopied(false), 1600);
  }

  function fakeDeploy() {
    setLoading(true);
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => setLoading(false), 1800);
  }

  const glowFilter = reduce ? "none" : undefined;

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 sm:py-28">
      <style>{`
        @keyframes bgr-pulse {
          0%, 100% { transform: scale(1); opacity: 0.55; }
          50% { transform: scale(1.06); opacity: 1; }
        }
        @keyframes bgr-spin {
          to { transform: rotate(360deg); }
        }
        .bgr-spin { animation: bgr-spin 0.8s linear infinite; }
        .bgr-ring-indigo,
        .bgr-ring-violet,
        .bgr-ring-emerald,
        .bgr-ring-rose,
        .bgr-ring-amber,
        .bgr-ring-sky {
          animation: bgr-pulse 1.6s ease-in-out infinite;
        }
        .bgr-ring-indigo { box-shadow: 0 0 0 2px rgba(99,102,241,0.9), 0 0 22px 4px rgba(99,102,241,0.55); }
        .bgr-ring-violet { box-shadow: 0 0 0 2px rgba(139,92,246,0.9), 0 0 22px 4px rgba(139,92,246,0.55); }
        .bgr-ring-emerald { box-shadow: 0 0 0 2px rgba(16,185,129,0.9), 0 0 22px 4px rgba(16,185,129,0.55); }
        .bgr-ring-rose { box-shadow: 0 0 0 2px rgba(244,63,94,0.9), 0 0 22px 4px rgba(244,63,94,0.55); }
        .bgr-ring-amber { box-shadow: 0 0 0 2px rgba(245,158,11,0.9), 0 0 22px 4px rgba(245,158,11,0.55); }
        .bgr-ring-sky { box-shadow: 0 0 0 2px rgba(14,165,233,0.9), 0 0 22px 4px rgba(14,165,233,0.55); }
        @media (prefers-reduced-motion: reduce) {
          .bgr-spin,
          .bgr-ring-indigo,
          .bgr-ring-violet,
          .bgr-ring-emerald,
          .bgr-ring-rose,
          .bgr-ring-amber,
          .bgr-ring-sky { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <header className="mb-14 text-center">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            FreeCode / Buttons
          </p>
          <h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
            Glow Ring Buttons
          </h2>
          <p className="mx-auto mt-4 max-w-xl text-sm text-slate-600 dark:text-slate-400">
            Hover or focus any button for a pulsing halo. Every control is a real{" "}
            <code className="rounded bg-slate-200 px-1.5 py-0.5 text-xs dark:bg-slate-800">
              &lt;button&gt;
            </code>{" "}
            with keyboard focus rings and live state.
          </p>
        </header>

        {/* Accent palette */}
        <div className="mb-12">
          <h3 className="mb-5 text-sm font-medium text-slate-500 dark:text-slate-400">
            Accents
          </h3>
          <div className="flex flex-wrap items-center justify-center gap-4">
            <GlowButton accent="indigo">Get started</GlowButton>
            <GlowButton accent="violet">Upgrade</GlowButton>
            <GlowButton accent="emerald">Publish</GlowButton>
            <GlowButton accent="sky">Connect</GlowButton>
            <GlowButton accent="amber">Boost</GlowButton>
            <GlowButton accent="rose">Go live</GlowButton>
          </div>
        </div>

        {/* Sizes */}
        <div className="mb-12">
          <h3 className="mb-5 text-sm font-medium text-slate-500 dark:text-slate-400">
            Sizes
          </h3>
          <div className="flex flex-wrap items-center justify-center gap-4">
            <GlowButton accent="indigo" size="sm">
              Small
            </GlowButton>
            <GlowButton accent="indigo" size="md">
              Medium
            </GlowButton>
            <GlowButton accent="indigo" size="lg">
              Large
              <ArrowIcon />
            </GlowButton>
          </div>
        </div>

        {/* Live interactive row */}
        <div className="mb-12">
          <h3 className="mb-5 text-sm font-medium text-slate-500 dark:text-slate-400">
            Live state
          </h3>
          <div className="flex flex-wrap items-center justify-center gap-4">
            {/* Like toggle */}
            <button
              type="button"
              onClick={toggleLike}
              aria-pressed={liked}
              aria-label={liked ? "Unlike" : "Like"}
              className={`group relative inline-flex h-11 select-none items-center gap-2 rounded-full border px-5 text-sm font-semibold transition-[transform,background-color,box-shadow] duration-200 active:scale-[0.97] ${FOCUS} ${
                liked
                  ? "border-rose-300 bg-rose-50 text-rose-700 hover:shadow-lg hover:shadow-rose-500/40 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300"
                  : "border-slate-300 bg-white text-slate-700 hover:shadow-lg hover:shadow-rose-500/30 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
              }`}
            >
              <span
                aria-hidden="true"
                className="bgr-ring-rose pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"
              />
              <span className="relative z-10 inline-flex items-center gap-2">
                <svg
                  className="h-4 w-4"
                  viewBox="0 0 24 24"
                  fill={liked ? "currentColor" : "none"}
                  stroke="currentColor"
                  strokeWidth="2"
                  aria-hidden="true"
                >
                  <path d="M12 21s-6.5-4.35-9.33-8.05C.9 10.2 1.36 6.9 3.7 5.3c1.9-1.3 4.4-.8 5.8.9L12 9l2.5-2.8c1.4-1.7 3.9-2.2 5.8-.9 2.34 1.6 2.8 4.9.9 7.65C18.5 16.65 12 21 12 21z" />
                </svg>
                <span className="tabular-nums">{likes}</span>
              </span>
            </button>

            {/* Subscribe toggle */}
            <button
              type="button"
              onClick={() => setSubscribed((v) => !v)}
              aria-pressed={subscribed}
              className={`group relative inline-flex h-11 select-none items-center gap-2 rounded-full px-6 text-sm font-semibold transition-[transform,background-color,box-shadow] duration-200 active:scale-[0.97] ${FOCUS} ${
                subscribed
                  ? "bg-slate-200 text-slate-700 hover:bg-slate-300 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700"
                  : "bg-emerald-600 text-white hover:bg-emerald-500 hover:shadow-lg hover:shadow-emerald-500/50"
              }`}
            >
              {!subscribed && (
                <span
                  aria-hidden="true"
                  className="bgr-ring-emerald pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"
                />
              )}
              <span className="relative z-10 inline-flex items-center gap-2">
                {subscribed ? (
                  <svg
                    className="h-4 w-4"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden="true"
                  >
                    <path d="M20 6 9 17l-5-5" />
                  </svg>
                ) : null}
                {subscribed ? "Subscribed" : "Subscribe"}
              </span>
            </button>

            {/* Copy */}
            <button
              type="button"
              onClick={copy}
              aria-label="Copy install command"
              className={`group relative inline-flex h-11 select-none items-center gap-2 rounded-full border border-slate-300 bg-white px-5 font-mono text-sm text-slate-700 transition-[transform,background-color,box-shadow] duration-200 active:scale-[0.97] hover:shadow-lg hover:shadow-sky-500/30 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 ${FOCUS}`}
            >
              <span
                aria-hidden="true"
                className="bgr-ring-sky pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"
              />
              <span className="relative z-10 inline-flex items-center gap-2">
                {copied ? (
                  <svg
                    className="h-4 w-4 text-emerald-500"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden="true"
                  >
                    <path d="M20 6 9 17l-5-5" />
                  </svg>
                ) : (
                  <svg
                    className="h-4 w-4"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    aria-hidden="true"
                  >
                    <rect x="9" y="9" width="11" height="11" rx="2" />
                    <path d="M5 15V5a2 2 0 0 1 2-2h10" />
                  </svg>
                )}
                {copied ? "Copied!" : "npm i motion"}
              </span>
            </button>
          </div>
        </div>

        {/* States */}
        <div>
          <h3 className="mb-5 text-sm font-medium text-slate-500 dark:text-slate-400">
            Loading &amp; disabled
          </h3>
          <div className="flex flex-wrap items-center justify-center gap-4">
            <GlowButton accent="violet" loading={loading} onClick={fakeDeploy}>
              {loading ? "Deploying" : "Deploy to prod"}
            </GlowButton>
            <GlowButton accent="indigo" disabled>
              Disabled
            </GlowButton>

            {/* Icon-only */}
            <button
              type="button"
              aria-label="Add to favorites"
              style={{ filter: glowFilter }}
              className={`group relative inline-flex h-11 w-11 select-none items-center justify-center rounded-full bg-amber-500 text-amber-950 transition-[transform,background-color,box-shadow] duration-200 active:scale-[0.97] hover:bg-amber-400 hover:shadow-lg hover:shadow-amber-400/50 ${FOCUS}`}
            >
              <span
                aria-hidden="true"
                className="bgr-ring-amber pointer-events-none absolute -inset-px rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"
              />
              <svg
                className="relative z-10 h-5 w-5"
                viewBox="0 0 24 24"
                fill="currentColor"
                aria-hidden="true"
              >
                <path d="M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.11 6.47L12 17.9l-5.81 3.07 1.11-6.47-4.7-4.58 6.5-.95z" />
              </svg>
            </button>
          </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 →