MynduiMyndui
0

Anatomy of the Magic Button

How a pushable 3D button is built from three flat layers, a springy bezier, and a rainbow that costs nothing when idle.

01/04The three-layer stack
Front face
what you press
Edge
fakes the thickness
Shadow
grounds it
tsx
<button className="group relative ..."><span className="absolute inset-0 rounded-xl ..." aria-hidden />   {/* shadow */}<span className="absolute inset-0 rounded-xl ..." aria-hidden />   {/* edge   */}<span className="relative block rounded-xl ...">{children}</span>  {/* face   */}</button>
01

The three-layer stack

There is no 3D geometry here. The depth is three stacked <span>s: a blurred shadow on the surface, a colored edge that fakes the side wall, and the front face you actually read. Lift the face a few pixels above the edge and your eye fills in a solid object.

Each layer is absolutely positioned in the same box, so they overlap perfectly at rest. Only their vertical offsets differ.

02/04The push physics
Push me
rest
hover
press
600ms
250ms
34ms
face -4px
face -6px
face -2px
tsx
// front face"-translate-y-[4px] [transition:translate_600ms_cubic-bezier(0.3,0.7,0.4,1)]group-hover:-translate-y-[6px]group-hover:[transition:translate_250ms_cubic-bezier(0.3,0.7,0.4,1.5)]group-active:-translate-y-[2px] group-active:[transition:translate_34ms]"
02

The push physics

Depth is static; push is motion. The face sits at -4px, lifts to -6px on hover, and dips to -2px on press — while the shadow deepens underneath. What sells it is the timing: a slow, springy settle at rest, a snappy overshoot on hover, and a near-instant 34ms dip on press.

The overshoot lives in the bezier: cubic-bezier(0.3, 0.7, 0.4, 1.5) — that final 1.5 control point pushes past the target and springs back.

Every offset animates translate, never top/margin or an animated box-shadow. Transforms are composited — the browser moves an already-painted layer on the GPU without re-running layout or paint. Animating top would re-flow the page every frame; animating box-shadow would re-paint it. This is the house rule across Myndui's motion, enforced in CI.

tsx
 
"[will-change:translate] [transition:translate_600ms_cubic-bezier(0.3,0.7,0.4,1)]"
03/04The rainbow edge

background-position slides across a 200%-wide gradient

tsx
"[background-image:linear-gradient(90deg,var(--rainbow-1),var(--rainbow-5),...)][background-size:200%_100%] animate-magic-rainbow motion-reduce:animate-none"
03

The rainbow edge

The optional rainbow isn't a video or a canvas — it's one gradient twice as wide as the button, slid sideways forever. background-size: 200% 100% gives the slack; a keyframe animates background-position across it.

motion-reduce:animate-none freezes the sweep for anyone who's asked the OS to cut motion.

An infinite background-position keyframe is main-thread paint — cheap, but not free, and pointless when the button is off screen. An IntersectionObserver pauses the animation's animationPlayState whenever the button scrolls out of view, and resumes it seamlessly on the way back in.

tsx
 
const io = new IntersectionObserver(
  ([entry]) => {
    for (const layer of root.querySelectorAll(".animate-magic-rainbow"))
      layer.style.animationPlayState = entry.isIntersecting ? "" : "paused";
  },
  { rootMargin: "128px" },
);

The 128px margin resumes it just before it re-enters, so you never catch it mid-restart.

04/04Result
04

The result

Every piece, assembled — the real component. Hover it, tab to it, press and hold. Each motion is one of the mechanisms above doing its job.

That's the whole illusion: three flat layers, one springy curve, a gradient on a timer, and the discipline to animate only what the GPU can move for free.

Accessibility

Real motion needs real semantics. The button tracks a pressed state from Enter/Space so keyboard users get the same dip as a mouse press; focus-visible mirrors the hover lift so the focused state reads as raised; and motion-reduce disables both the rainbow and — via the OS setting — the springy transitions.

tsx
 
const handleKeyDown = (e) => {
  if (e.key === "Enter" || e.key === " ") setPressed(true);
};
// focus-visible gets the same lift as hover:
"group-focus-visible:-translate-y-[6px]";

Motion Score

Magic ButtonCCPaint-triggering
StranslatePush physics — shadow + front face lift and dip
SfilterBrightness shift on hover / focus-visible
Cbackground-positionRainbow gradient keyframe loop
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 →