A dark pill button whose border is a band of cyan, violet and pink light that circles it continuously. The same light is repeated underneath as a blurred glow, and a pool of colour breathes on the floor below, so the button looks lit from within. It sits in a centred dark hero with a headline, a secondary link and a row of facts. Hovering or focusing the button doubles the rotation speed. Everything stacks into one column on phones.
import { useEffect, useRef, useState } from "react";
// One full turn of the light, in seconds. Hovering the button halves it.
const TURN_SECONDS = 4;
const shortcuts = [
{ keys: "git push", text: "deploys in 9 seconds" },
{ keys: "38 regions", text: "one config file" },
{ keys: "Rollback", text: "a single click" },
];
export default function ButtonWithRotatingGradientGlowBorder() {
const rootRef = useRef<HTMLElement>(null);
const [paused, setPaused] = useState(false);
// Stop the rotation while the section is scrolled out of view.
useEffect(() => {
const el = rootRef.current;
if (!el || typeof IntersectionObserver === "undefined") return;
const observer = new IntersectionObserver(([entry]) => setPaused(!entry.isIntersecting));
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<section
ref={rootRef}
data-paused={paused}
className="button-with-rotating-gradient-glow-border relative overflow-hidden bg-slate-950 px-5 py-20 text-white sm:px-8 sm:py-24"
>
<style>{`
@keyframes button-with-rotating-gradient-glow-border-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes button-with-rotating-gradient-glow-border-rise {
from { opacity: 0; transform: translateY(18px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes button-with-rotating-gradient-glow-border-floor {
0%, 100% { opacity: 0.55; transform: scaleX(0.9); }
50% { opacity: 0.9; transform: scaleX(1.08); }
}
.button-with-rotating-gradient-glow-border .rgb-rise {
animation: button-with-rotating-gradient-glow-border-rise 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* The light: a square far larger than the button, painted with two comet tails, spinning about its centre. */
.button-with-rotating-gradient-glow-border .rgb-light {
position: absolute;
left: 50%;
top: 50%;
width: 220%;
aspect-ratio: 1 / 1;
background: conic-gradient(from 0deg, rgba(34, 211, 238, 0) 0deg, rgba(34, 211, 238, 0) 40deg, #22d3ee 95deg, #a78bfa 130deg, #f472b6 160deg, rgba(244, 114, 182, 0) 180deg, rgba(244, 114, 182, 0) 235deg, #818cf8 290deg, #22d3ee 335deg, rgba(34, 211, 238, 0) 360deg);
animation: button-with-rotating-gradient-glow-border-spin ${TURN_SECONDS}s linear infinite;
will-change: transform;
}
.button-with-rotating-gradient-glow-border .rgb-button:hover .rgb-light,
.button-with-rotating-gradient-glow-border .rgb-button:focus-visible .rgb-light {
animation-duration: ${TURN_SECONDS / 2}s;
}
.button-with-rotating-gradient-glow-border .rgb-floor {
animation: button-with-rotating-gradient-glow-border-floor ${TURN_SECONDS}s ease-in-out infinite;
}
.button-with-rotating-gradient-glow-border[data-paused=true] * {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.button-with-rotating-gradient-glow-border .rgb-rise,
.button-with-rotating-gradient-glow-border .rgb-floor { animation: none; }
.button-with-rotating-gradient-glow-border .rgb-light { animation: none; transform: translate(-50%, -50%) rotate(20deg); }
}
`}</style>
{/* Night sky: faint grid, fading toward the edges, and one cool pool of light */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-30"
style={{
backgroundImage: "linear-gradient(rgba(148,163,184,0.16) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,0.16) 1px, transparent 1px)",
backgroundSize: "56px 56px",
maskImage: "radial-gradient(ellipse at center, black 20%, transparent 72%)",
WebkitMaskImage: "radial-gradient(ellipse at center, black 20%, transparent 72%)",
}}
/>
<div aria-hidden="true" className="pointer-events-none absolute left-1/2 top-0 h-80 w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-600/30 blur-3xl" />
<div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
<p className="rgb-rise inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium text-slate-300">
<span className="rounded-full bg-cyan-400/20 px-2 py-0.5 text-xs font-semibold text-cyan-300">New</span>
Orbit Relay 3.0 is out of beta
</p>
<h2 className="rgb-rise mt-6 text-4xl font-semibold leading-tight tracking-tight sm:text-6xl" style={{ animationDelay: "0.08s" }}>
Ship at midnight.
<br />
<span className="bg-gradient-to-r from-cyan-300 via-violet-300 to-pink-300 bg-clip-text text-transparent">Sleep through the launch.</span>
</h2>
<p className="rgb-rise mt-6 max-w-xl text-base leading-7 text-slate-300 sm:text-lg" style={{ animationDelay: "0.16s" }}>
Push to main and Orbit Relay builds, tests and rolls out to every region while the light on this button keeps circling.
</p>
<div className="rgb-rise relative mt-12 flex flex-col items-center gap-5 sm:flex-row" style={{ animationDelay: "0.26s" }}>
<div className="relative">
{/* Glow under the button: the same light, blurred */}
<div aria-hidden="true" className="pointer-events-none absolute -inset-1 overflow-hidden rounded-full opacity-80 blur-xl">
<span className="rgb-light" />
</div>
{/* Light pooling on the floor */}
<div aria-hidden="true" className="rgb-floor pointer-events-none absolute -bottom-10 left-1/2 h-8 w-4/5 -translate-x-1/2 rounded-full bg-gradient-to-r from-cyan-500/50 via-violet-500/60 to-pink-500/50 blur-2xl" />
<button
type="button"
className="rgb-button group relative isolate inline-flex h-16 items-center overflow-hidden rounded-full p-0.5 text-lg font-semibold text-white transition-transform duration-300 ease-out hover:scale-105 active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 focus-visible:ring-offset-4 focus-visible:ring-offset-slate-950"
>
<span aria-hidden="true" className="absolute inset-0 -z-20 rounded-full bg-white/10" />
<span aria-hidden="true" className="rgb-light -z-10" />
<span className="relative inline-flex h-full items-center gap-3 rounded-full bg-slate-950 pl-8 pr-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.12)]">
<span aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-full bg-gradient-to-b from-white/10 to-transparent" />
<span className="relative">Start deploying</span>
<span className="relative flex h-10 w-10 items-center justify-center rounded-full bg-white text-slate-950 transition-transform duration-300 ease-out group-hover:translate-x-0.5">
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M4 10h11m-4-4.5L15.5 10 11 14.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</span>
</button>
</div>
<a
href="#"
className="inline-flex h-12 items-center rounded-full px-5 text-base font-medium text-slate-300 transition-colors hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
Read the changelog
</a>
</div>
<ul className="rgb-rise mt-20 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-slate-400" style={{ animationDelay: "0.36s" }}>
{shortcuts.map((item) => (
<li key={item.keys} className="flex items-center gap-2">
<span className="rounded-md border border-white/10 bg-white/5 px-2 py-1 font-mono text-xs text-slate-200">{item.keys}</span>
{item.text}
</li>
))}
</ul>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/button-with-rotating-gradient-glow-border.json
The button clips its content with `overflow-hidden` and has 2px of padding. Behind the inner face sits a square much larger than the button, painted with a `conic-gradient` and rotated by a keyframe. Only the 2px ring of that square stays visible, so it reads as a moving border.
Animating the angle needs a registered custom property, and it repaints the gradient on every frame. Rotating a layer is a transform, which browsers handle on the compositor, so it stays smooth on slow devices.
A second copy of the spinning square sits in a wrapper slightly larger than the button with a blur filter. Because both copies run the same animation, the glow always matches the border.
That comes from rotating a conic gradient inside a wide shape. A shorter, rounder button reduces it. For most buttons the uneven speed looks natural, like a light sweeping past.
No. It imports only React, for the observer that pauses the animation while the section is off screen.
Related: Animated Gradient Border Card Wrapper, Shimmer Button with Light Sweep in Three Sizes, Breathing Glow Border Status Panel.
More animated buttons or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.