GodUIGodUI
129Follow on X

Anatomy of the Shimmer Button

How a single rotating conic gradient becomes a border-only light sweep, and why it speeds up under your cursor without restarting.

01/03The three-layer stack

spark only

spark + cut

+ label

Spark
conic gradient, blurred, spinning
Cut
solid inset that hides the middle
Label
text, always on top
tsx
<div ref={sparkRef} className="absolute inset-0 -z-30 overflow-visible blur-[2px] [container-type:size]"><div className="animate-shimmer-slide absolute inset-0 aspect-square h-[100cqh] [mask:none]">  <div className="animate-spin-around absolute -inset-full w-auto [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]" /></div></div><span className="relative z-10">{children}</span><div className="absolute -z-20 [inset:var(--cut)] [border-radius:var(--radius)] [background:var(--bg)]" />
01

The three-layer stack

No border is drawn. The shimmer is a spinning gradient blob, mostly hidden behind a solid cutout, so only a sliver of it reaches the edge. Every button renders three things in the same box: a spark (a blurred conic gradient that spins and slides), a cut (a solid inset the exact color of the background), and the label. The spark fills the entire button. The cut sits on top of it and covers everything except a thin ring at the edge. That ring is the only part of the spark you see.

The stacking context does the work: spark at -z-30, cut at -z-20, label at z-10. There is no border, only paint that is partially covered.

The spark layer is two nested elements, each running its own keyframe. The outer one, shimmer-slide, translates the whole gradient sideways across 100cqw (container width) with ease-in-out infinite alternate, so it slides right, then back left, forever. The inner one, spin-around, rotates the conic gradient itself through a 0° → 90° → 270° → 360° timeline, linear infinite, at calc(var(--speed) * 2), twice as slow as the slide. Together they make the light orbit instead of bouncing.

css
 
--animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite alternate;
--animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;

--speed defaults to 3s and is a plain CSS variable. shimmerDuration sets it directly, with no re-render.

02/03Speeding up without restarting

leave / blur: rate 1×

hover / focus-visible: rate 3×

Rate 1×
resting state, ambient shimmer
Rate 3×
hover / keyboard focus, same timeline
tsx
const setSparkRate = (rate: number) => {sparkRef.current?.getAnimations({ subtree: true }).forEach((a) => {  a.playbackRate = rate;});};const handleMouseEnter = () => setSparkRate(3);const handleMouseLeave = () => setSparkRate(1);
02

Speeding up without restarting

Hovering doesn't swap in a faster animation. The same Web Animations API timeline keeps running; the button asks the browser to play it back three times as fast.

Because playbackRate scrubs the existing timeline rather than restarting it, the gradient never jumps or resets position when you hover. It picks up speed from wherever it already was. Keyboard focus gets the same treatment, but only for :focus-visible. Programmatic or pointer-driven focus doesn't trigger the speed-up, so it stays keyboard-only:

tsx
 
const handleFocus = (e: React.FocusEvent<HTMLButtonElement>) => {
  if (e.target.matches(":focus-visible")) setSparkRate(3);
};

Enter and Space fire a native click but never toggle CSS :active, so the component tracks it manually with data-pressed, matching the same translate-y-px the mouse gets from active:translate-y-px:

tsx
 
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
  if (e.key === "Enter" || e.key === " ") setPressed(true);
};
tsx
 
className="... active:translate-y-px data-[pressed=true]:translate-y-px ..."

One rule, two triggers: pointer :active and the synthetic data-pressed land on the same transform.

03/03Result
03

The result

A spinning gradient, a solid cutout, a label on top, and one line that scrubs a Web Animations timeline instead of swapping animations.

Accessibility

Every decorative layer (spark, its inner slide, and the empty overlay) carries motion-reduce:animate-none, so prefers-reduced-motion users get a flat, static button with no gradient at all. The cut and spark also disappear entirely when shimmer={false} or the button is disabled, via a shared Tailwind group selector rather than duplicated conditionals:

tsx
 
const HIDDEN_WHEN_OFF =
  "group-[&:not([data-shimmer=true])]:hidden group-disabled:hidden";

Focus still gets a real ring (focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2), independent of the shimmer speed-up, so turning off the shimmer never removes the focus indicator.

Motion Score

Shimmer ButtonSS: Compositor-only
StranslateShimmer sweep via translated highlight
SopacityHighlight fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →