On this page
Quick start
$ npm i wooomsimport { Woom } from "@woooms/react";
<Woom variant="jelly">
<button>Buy now</button>
</Woom>Install @woooms/react too. <Woom> wraps one child without adding an element, works in the Next.js App Router, and sets data-woom on the server, so the CSS fallback works before hydration. Prefer a hook? useWoom(options) returns a callback ref.
Variants
Start here
A dozen to try first, from every corner of the catalogue.
Slice
A juicy fruit, sliced in two and sealed again.
data-woom="slice"Snake
An old phone's screen: a pixel snake eats your label, then follows you round it.
data-woom="snake"Nixie
Nixie tubes: your label in glowing neon wire that flickers on, racing on press.
data-woom="nixie"Split-flap
A departure board: every cell clatters through characters and lands on your label.
data-woom="splitflap"Coin
Minted in metal: flips to a check, spins while it loads, or just flies.
data-woom="coin"Ferrofluid
Magnetic fluid wells up under the pointer and follows it, spiking when pressed.
data-woom="ferrofluid"Vinyl
A DJ's turntable: the record spins up, the needle drops, and a press scratches it.
data-woom="vinyl"Defuse
A planted bomb, beeping faster: hold to defuse it.
data-woom="defuse"Saber
A laser sword's hilt with your label on it: the blade ignites; a press clashes sparks.
data-woom="saber"Invaders
An arcade screen: pixel aliens march on your cannon; press to fire.
data-woom="invaders"Glass
A thick glass slab: the page behind bends through it.
data-woom="glass"Typewriter
A sheet in a typewriter: your label typed in uneven ink, then the bell and the return.
data-woom="typewriter"Everyday
For the jobs every site has: holding to confirm, sending, copying, buying, deleting, uploading.
Hold to confirm
For delete and pay: hold it and its face fills as it sinks and trembles, then thuds.
data-woom="hold"Send
For send buttons: a paper plane lifts off the face and is thrown away; it says Sent.
data-woom="send"Copy
For copy buttons: a sheet flies into a clipboard and the label says Copied.
data-woom="copy"Trash
A bin beside the label; pressed, the label crumples and goes in.
data-woom="trash"Cart
A cart rolls in; each press drops a parcel in and counts it.
data-woom="cart"Upload
For upload buttons: a sheet flies up into a cloud as a bar fills, then a tick.
data-woom="upload"Bell
For subscribe buttons: a bell beside the label rings when pressed; it says Subscribed.
data-woom="bell"Card
For big things you click: lifts, tilts and catches the light.
data-woom="card"In the core
Part of the renderer itself: nothing more to download.
Pop
Springs out of the page as a solid block, leans back and sinks when pressed.
data-woom="pop"Keycap
A mechanical key: rises on its skirt, travels down to a hard stop, clacks.
data-woom="keycap"With sound on, the keycap clacks on the way down and, more softly, on the way back. The sound is synthesised with Web Audio, so there's nothing to download, and the audio context is only created from a real press, never on load. Keyboard users get a CSS press (:focus-visible:active) instead, so the focus ring stays on screen.
<button data-woom="keycap" data-woom-sound>Enter</button>Classics
The four that started it.
Jelly
A gummy sweet: glows where light gets in, wobbles, ripples under you.
data-woom="jelly"Chrome
Polished metal mirroring a studio; press for a liquid ripple.
data-woom="chrome"Holographic
A holo foil card: colours, stars and glare follow the pointer.
data-woom="holographic"Spring
A jack-in-the-box: out it boings on a real steel coil.
data-woom="spring"Games
Nods to the games everyone has played.
Sushi
A maki roll, chopped into slices that tip over to show what's inside.
data-woom="sushi"Bamboo
A clean cut, a beat of nothing, then the top slides off.
data-woom="bamboo"Rings
Gold rings spin over the label; a press knocks it back and scatters them bouncing.
data-woom="rings"TNT
A red block with a lit fuse that flashes, swells and blows into cubes.
data-woom="tnt"Guess
Letter tiles that flip to grey, yellow and green, then hop when solved.
data-woom="guess"K.O.
A fight HUD: three hits drain the bar and a huge K.O. slams in.
data-woom="ko"Pentakill
A multi-kill callout: DOUBLE, TRIPLE, QUADRA, then your label in gold with a flare.
data-woom="pentakill"Wasted
Game over: everything slows and drains to grey, and WASTED comes down in red.
data-woom="wasted"Case
A case-opening roll: it races, slows, and lands your label on gold.
data-woom="case"Emergency
A big red button under a glass dome: press it and the alarm goes off.
data-woom="emergency"Crafting
A crafting window: ingredients drop into the grid, and a press crafts your label.
data-woom="crafting"Recall
Hold to go home: a ring of blue runes fills, and it blinks out in a column of light.
data-woom="recall"Cast bar
A spell's cast bar: hold to cast, and it fires with sparks and a floating +42.
data-woom="castbar"Alert
A stealth game's radar: a press pops a red ! and ALERT counts down.
data-woom="alert"Age up
A research button in wood; each press advances it an age: wood, stone, then gold.
data-woom="ageup"Creeper
A mottled green face that hisses, flashes and bursts into cubes.
data-woom="creeper"Loot
An item tooltip with a rarity glow; each press rolls it up, from grey to legendary.
data-woom="loot"Ward
Fog of war over the button; a ward plants and its circle of vision shows your label.
data-woom="ward"Catch
A capture ball by the label; press, and the button is pulled in: three rocks, caught.
data-woom="catch"Pipe
A green warp pipe rises under it; press, and it sinks down the pipe and pops back up.
data-woom="pipe"Mission passed
The job's done: the page washes warm and MISSION PASSED slams in, your label as prize.
data-woom="passed"You died
Everything fades to black and YOU DIED fades in, slowly, in thin red capitals.
data-woom="died"Evolve
Flashes into a glowing silhouette, faster and faster, then bursts back as itself.
data-woom="evolve"Slingshot
Drawn back on a wooden slingshot and launched.
data-woom="slingshot"Crosshair
A green crosshair for a pointer; click to fire: recoil, a hitmarker, a bullet hole.
data-woom="crosshair"Screens and pixels
Old machines and the way they drew.
Chomp
A maze arcade screen: a chomper clears the dots round your label, ghosts on its tail.
data-woom="chomp"Dino
The offline runner game: a pixel dinosaur jumps cacti under your label; press to jump.
data-woom="dino"Teletext
A page of 80s TV text: the counter rolls, the page loads line by line; press to flip.
data-woom="teletext"Bluescreen
A monitor that crashes to blue when pressed, your label as the error, then reboots.
data-woom="bluescreen"Flip-dot
A destination sign of flipping discs: your label in dot letters, scrolling if long.
data-woom="flipdot"CRT
A cathode-ray tube: it switches on, the pointer bends its colours; a press degausses.
data-woom="crt"Glitch
Comes out in a burst of interference, then holds still but for a flicker now and then.
data-woom="glitch"Time circuits
A time machine's dashboard: red, green and amber displays power on; press for sparks.
data-woom="timecircuits"Calculator
A pocket calculator: your label keyed in from the right in seven segments.
data-woom="calculator"LCD
A little gadget: your label in dots on a screen that powers on.
data-woom="lcd"Pocket pet
A 90s virtual pet: it hatches, follows the pointer and jumps for joy when pressed.
data-woom="pet"Dex
A red pocket encyclopedia: the lens lights up, your label types in, a press scans.
data-woom="dex"Stack
The falling-blocks well: pieces drop and lock into your label; press for a line clear.
data-woom="stack"Grass block
A blocky game's grass block: it tilts to show its top; a press mines it into cubes.
data-woom="grass"Radar
A pocket tracker: a domed green grid with blinking dots that slide as you point it.
data-woom="radar"Wheel
A pocket music player: your label is the song playing; the wheel ticks as you turn.
data-woom="wheel"Neon
A bar's neon sign on brick: your label in glowing tube, flickering on, a letter late.
data-woom="neon"Dialup
A 90s dial-up window: dialling, verifying, connected, modem lights flickering.
data-woom="dialup"Nudge
A little 2000s chat window. Press to shake it, buzz and all.
data-woom="nudge"Visualizer
A 2000s music player's skin: a dancing spectrum; a press drops the beat.
data-woom="visualizer"VHS
A tape on an old telly, its tracking wobbling, the player's display; press to pause.
data-woom="vhs"Vision
A machine's red heads-up view, your label the target: it locks on, a press says MATCH.
data-woom="vision"Signal
A searchlight sweeps up over the skyline and throws your label onto the clouds.
data-woom="signal"Refine
An eerie office terminal: point at the drifting numbers, press to sort them in a bin.
data-woom="refine"Crawl
Your label in yellow capitals, crawling up and away into a field of stars.
data-woom="crawl"Rain
Green digital rain pours down a black screen and leaves your label glowing in it.
data-woom="rain"Lights
Christmas lights over painted letters spell your label, and go wild on press.
data-woom="lights"Question block
The golden "?" block: hit it and a coin pops out of the top.
data-woom="question"Terminal
The whole 3D button drawn in characters, as a terminal draws a shape, over the page.
data-woom="terminal"One-bit
The whole 3D button in one bit, as an old Mac game drew it: sides, bevel and shadow.
data-woom="onebit"Pixel
Resolves from big pixels to sharp, a staircase outline, pixels trailing the pointer.
data-woom="pixel"Dither
Lit by the pointer as by a lamp, the light printed in dither or halftone dots.
data-woom="dither"ASCII
The pointer's light written in characters: they decode, and rewrite as you move.
data-woom="ascii"Mosh
Datamosh: the pointer's motion drags the face's blocks, which snap back one by one.
data-woom="mosh"Pixel sort
The glitch artist's sort: its pixels sorted into streaks where the pointer passes.
data-woom="pixelsort"Scanner
A glossy black panel: a red light sweeps back and forth behind smoked glass.
data-woom="scanner"Pin art
A pin-art toy: pins rise round the label, and the pointer leaves its print in them.
data-woom="pinart"Mechanisms
Things that open, lift, swing and fly.
Chest
A treasure chest: it rattles, then opens in a blaze of gold and your label rises out.
data-woom="chest"Eight ball
The fortune-telling ball: shake it and your label floats up on the die.
data-woom="eightball"Twisty cube
The colour puzzle cube: it scrambles, then solves itself onto your label.
data-woom="twisty"Tape
A mixtape: your label in marker on the cassette, the hubs turning; press to wind on.
data-woom="tape"Cartridge
A grey game cartridge with your label as the title: blow on it, push it in, it boots.
data-woom="cartridge"Reactor
A glowing chest reactor in armour plate: it powers up coil by coil; a press fires.
data-woom="reactor"Balloon
Blows up into a balloon and floats on its string, bobbing after the pointer.
data-woom="balloon"Dalgona
Honeycomb candy in your label's shape: press to crack it, press again to pop it out.
data-woom="dalgona"Receipt
A thermal printer: a receipt prints with your label as the shop's name, torn on press.
data-woom="receipt"Not a robot
The human check: tick the box, watch it spin, get your green tick.
data-woom="robot"Eye
A starship computer's calm red eye in brushed metal: it watches you, flares on press.
data-woom="eye"Level up
A level badge and an XP bar; a press fills it: a column of gold light, and a level up.
data-woom="levelup"Achievement
An achievement-unlocked toast slides out of its medal; a press spins the medal, ding.
data-woom="achievement"Arcade
A real push button: a glossy domed cap in a collar, with a hard click.
data-woom="arcade"Bobblehead
A bobblehead: it nods and shakes its head on its spring neck as you move.
data-woom="bobble"Levitate
Floats over a maglev base on a field of light that pushes back when pressed.
data-woom="levitate"Parallax
Like a TV's home-screen icon: the label floats off the button and tilts with a glare.
data-woom="parallax"Layers
Explodes into its layers, as a design tool shows them, guides and selection included.
data-woom="layers"Pop-up
Stands up off the page like a pop-up book's cut-out, on folded paper strips.
data-woom="popup"Drawer
Slides out of the page like a drawer, to its stop; push it shut and it springs back.
data-woom="drawer"Lid
A hinged lid: it opens on a box in your button's own colour, light round its rim.
data-woom="lid"Label maker
Embossed label tape: your label punched in raised white letters, a click at a time.
data-woom="labelmaker"Sketch
The red drawing toy: one unbroken line draws your label; shake it clean.
data-woom="sketch"Beachball
The spinning rainbow wait cursor: your button greys out busy; press and it comes back.
data-woom="beachball"Badge
The red counter: 1, 2, 3… 99+. Press to clear it with a pop.
data-woom="badge"Typing
Three dots typing, then your label arrives as the message. Heart it.
data-woom="typing"Scissor lift
Rides up on a scissor lift: crossed steel arms swing up, and fold flat when pressed.
data-woom="scissor"Bellows
Rises on a pleated bellows that bends as you move and puffs when squeezed.
data-woom="bellows"Plunger
A pinball launcher: hold to pull it back, let go and it fires into its stop.
data-woom="plunger"Hanging sign
Hangs off the page on a cord, like a shop's door sign, and swings when you push it.
data-woom="sign"Flag
A piece of silk in a breeze, its folds catching the light; the pointer is a gust.
data-woom="flag"Progress
For actions that take a while: light fills its edge as it goes, a flash when done.
data-woom="progress"Cursor
The pointer joins its world: floating above, casting a shadow, touching down on press.
data-woom="cursor"Portal
A window into a tunnel running back into the page, real depth; a press rushes down it.
data-woom="portal"Surfaces
What it's made of, and how the light plays on it.
Scratch card
A scratch-off ticket: rub the foil away to find your label, press to win.
data-woom="scratch"Water
A shallow pool: the pointer raises ripples that bounce off its edges, bending it.
data-woom="water"Liquid
Liquid glass: a clear lens with a bright rim, the page bending through its edge.
data-woom="liquid"Thermal
Through a thermal camera: your pointer's heat lingers and spreads, the sights read it.
data-woom="thermal"Oil slick
A film of oil on water: iridescent, it stirs with the pointer and ripples on press.
data-woom="oilslick"Amber
A polished chunk of amber, an insect and your label inside; light glows through it.
data-woom="amber"Stamp
A wooden stamp: slam it down, it leaves your mark in ink.
data-woom="stamp"Instant
An instant photo: it slides out, your label in marker, and a sunset develops on it.
data-woom="instant"Aurora
A panel of night sky: curtains of light ripple, lean after you and flare on press.
data-woom="aurora"Watercolour
Watercolour: colour floods into wet paper from where you came in, a blot on press.
data-woom="ink"Clay
Modelling clay: press for a fingerprint, drag for a groove.
data-woom="clay"Dreamy
Dreamy retrofuturism: pastel glitter that shifts as it tilts, or an old sci-fi print.
data-woom="dreamy"Psychedelic
A 70s poster come alive: liquid colour swirls round the pointer, melting on press.
data-woom="psychedelic"Highway
A green motorway sign: headlights sweep across it, and a press lights the exit arrow.
data-woom="highway"Map
Folded parchment inks your label in with a flourish, footprints walking across it.
data-woom="map"Element
A title card in green smoke: your label's element symbols lifted into periodic tiles.
data-woom="element"Backlit
A backlit key: light leaks from under it and lights its label. Best on dark pages.
data-woom="backlit"Edge
Precise and quiet: a thin slab whose edge catches the light, a hairline ring on press.
data-woom="edge"Beam
A beam of light runs round its edge, a spotlight follows the pointer. For dark sites.
data-woom="beam"A coin with a real state
While it shows a state it stays out even if the pointer leaves. aria-busy is also what screen readers hear, so the state is never only in the effect.
<Woom variant="coin" kind="spin">
<button onClick={pay} aria-busy={paying} data-woom-state={paid ? "done" : undefined}>
{paying ? "Paying…" : paid ? "Paid" : "Pay now"}
</button>
</Woom>Not just buttons
This page is a studio too. Type a word anywhere, drop a photo or paste your website's address, and it comes out onto the page in 3D. Drag a card onto it to make it of that, or into the bin to throw it away.
On a wide screen, with motion allowed, this page is a studio too: type a word anywhere, drop a photo or paste your website's address, and it comes out onto the page in 3D, to be made of any of the variants above.
Controls
The toggle variant is for controls that latch: switches, checkboxes, tabs and radios rise on hover, clack down when pressed and stay sunk in while on, with a switch's knob and a checkbox's tick drawn in 3D. A slider is a long switch whose knob follows the value as you drag it, ticking as it goes. The React package comes with them ready-made: Switch, Checkbox, RadioGroup, Segmented, Tabs, ToggleButton, Chip, Accordion, Slider, RangeSlider, Stepper, Rating, OptionCards, SplitButton, Pagination and Swatches, with their stylesheet.
import { Checkbox, Segmented, Slider, Switch } from "@woooms/react/controls";
import "woooms/controls.css";
<Switch label="Sound" checked={sound} onCheckedChange={setSound} />
<Slider label="Volume" value={volume} onValueChange={setVolume} />
<Checkbox defaultChecked>Speed lines</Checkbox>
<Segmented
legend="Size"
options={[{ value: "sm", label: "SM" }, { value: "md", label: "MD" }]}
value={size}
onValueChange={setSize}
/>Each works controlled or not. Keep their sizes as they are: the shader draws the knob and the tick at the same geometry as the CSS.
Bursts
Any variant can throw something when it's pressed: confetti, sparks, hearts or bubbles, in real 3D, out of the spot you pressed and away down the page. It also goes off when the button becomes done (data-woom-state="done"), from its middle. Press again and again and each press throws its own volley while the others finish their fall; the button stays out till the last piece is gone. With sound on, each has its own.
Confetti
Paper strips thrown out towards you, tumbling and fluttering down the page.
Sparks
A flash, and white-hot sparks that streak out, cooling to orange and red.
Hearts
Puffy 3D hearts pop out, then float up swaying and shrink away.
Bubbles
Soap bubbles blown out of it, drifting up in a film's colours till each pops.
<button data-woom="keycap" data-woom-burst="confetti">Publish</button>
<script type="module">
import { autoWoom } from "woooms";
autoWoom();
</script>Like the variants, each works by name, and its code is only fetched once a button that uses it is hovered. Visitors who ask for reduced motion get no 3D at all, so no bursts either.
Your own variant
Every variant is written against the same plugin API, and yours can be too, registered under a name of its own (or one of theirs, to put yours in its place). A variant is a plain object: its default options, springs, a pose function (how far the block lifts, leans and tilts for a given pop, press and pointer), and optionally its own per-button physics, GLSL hooks to bend or shade the block, and a sound.
import { defineVariant, registerVariant } from "woooms";
const float = defineVariant("float", {
defaults: { depth: 0.3, lean: 0.1, tilt: 0.25, taper: 0, lines: false, sound: false },
springs: {
pop: { stiffness: 120, damping: 14 },
pressDown: { stiffness: 600, damping: 30 },
pressUp: { stiffness: 600, damping: 30 },
tilt: { stiffness: 80, damping: 12 },
},
pose: ({ pop, press, tiltX, tiltY, height, options }) => ({
y: height * 0.2 * pop,
z: height * 0.8 * pop * (1 - 0.5 * press),
rotX: (-options.lean + options.tilt * tiltX) * pop,
rotY: options.tilt * tiltY * pop,
rotZ: 0,
scaleZ: Math.max(0.001, pop),
taper: 0,
dish: 0,
}),
wallShade: 0.4,
bevel: (h) => h * 0.08,
bottomOut: false,
lineSpeed: 0,
});
registerVariant("float", float);The pose must be flat (all zeros) at pop: 0, so the 3D twin matches your button exactly when it appears and disappears. GLSL hooks are documented on the VariantDef type.
Options
Precedence, lowest to highest: defaults, then CSS variables, then data-woom-* attributes, then JavaScript options.
| Option | Type | Default | What it does |
|---|---|---|---|
variant | "pop" | "keycap" | registered name | "pop" | Which effect. Picks that variant's defaults for everything below. |
kind | string | the variant's first | For variants that come in kinds: which one (a coin: "flip", "spin" or "toss"). |
depth | number | 0.8 / 0.45 | Block depth as a fraction of the button's height. |
lean | number | 0.42 / 0.14 | How far the face leans back when fully out, in radians. |
tilt | number | 0.2 / 0.06 | How much the block turns to follow the pointer, in radians. |
taper | number | 0.14 / −0.1 | Positive: the face grows (+14%). Negative: the base flares out instead and the face keeps the button's size. |
lines | boolean | true | Speed lines running along the walls (pop has them; keycap only if you ask). |
burst | string | What a press throws, on any variant: confetti, sparks, hearts or bubbles. See Bursts. | |
sound | boolean | false | The variant's sound. Keycap: a clack when it bottoms out and when it comes back. |
depthColor | CSS colour | background, darkened | Wall colour. |
lineColor | CSS colour | deeper wall colour | Speed line colour. |
shadowTint | CSS colour | near-black + wall | Shadow colour. |
reducedMotion | "user" | "always" | "never" | "user" | Override the motion preference for this button. |
out | boolean | false | Keep it out in 3D all the time, not only while hovered: for a headline or a showpiece. The pointer still tilts it and a press still presses it. HTML: data-woom-out. |
onPop | (el) => void | Called when the block starts popping out. | |
onSink | (el) => void | Called when the popped-out block is pressed in. |
Theming
CSS variables are read from the button's computed style each time it's hovered, so they follow your themes, dark mode and media queries. Any colour the browser understands works: color-mix(), oklch(), var(), named colours. woooms lets the browser paint the colour into a 1×1 canvas and reads the pixel back, rather than parsing CSS itself.
--woom-depth-color | Wall colour |
--woom-lines | Speed line colour, or none to turn them off |
--woom-shadow-tint | Shadow colour |
--woom-depth | Same as the depth option |
--woom-lean | Same as the lean option |
--woom-tilt | Same as the tilt option |
--woom-taper | Same as the taper option |
Page-wide settings
import { configure, preload, isSupported, stats } from "woooms";
configure({
zIndex: 40, // overlay canvas base z-index
reducedMotion: "user", // "user" | "always" | "never"
maxDpr: 2, // device pixel ratio cap
shadowStrength: 1, // how dark every shadow is; 0 for none
shadowSoftness: 1, // how soft their edges are; 2 is twice the blur
shadowLength: 1, // how far they fall, as if the light were lower
shadowGrain: 0, // a film grain in them, 0 to 1
});
preload(); // fetch the renderer now (e.g. on idle) instead of on first hover
isSupported(); // true when this visitor gets the WebGL effect
stats(); // debug counters: loaded, live blocks, average frame costAccessibility
- The effect is decoration only. Nothing it shows carries meaning that the button itself doesn't.
- The canvas is
aria-hiddenandpointer-events: none. Clicks, taps and keys always reach your element. - A button showing a keyboard focus ring (
:focus-visible) never pops, so the ring is never hidden. The fallback stylesheet gives focused buttons a small 2px lift instead. disabledandaria-disabled="true"buttons are left alone.- Reduced motion is honoured by default, both by the WebGL layer and by the CSS fallback.
Support & fallbacks
The WebGL effect needs WebGL2, a mouse (hover: hover and pointer: fine) and motion allowed. Everything else degrades:
| Where | What you get | Notes |
|---|---|---|
| Desktop Chrome / Edge, mouse | WebGL effect | Tested (Chromium, CI with SwiftShader and on a real GPU) |
| Desktop Firefox, mouse | WebGL effect | Expected; not yet verified |
| Desktop Safari 16.4+, mouse | WebGL effect | Expected; not yet verified |
| Phones, tablets, any touch input | Your button as is | No hover on touch, so nothing to pop |
| prefers-reduced-motion: reduce | Your button as is | Neither WebGL nor CSS motion |
| No WebGL2 / renderer fails to start | CSS fallback | Tested |
| Button inside a modal <dialog> or popover | WebGL effect | The canvas joins the top layer while it's out (popovers needed; otherwise CSS fallback) |
| WebGL context lost | Button restored at once, 3D on next hover | Gives up after 3 losses → CSS fallback |
While WebGL is in charge, html has a data-woom3d attribute; the fallback CSS stays out of the way. A popped-out button has data-woom-live; a button handed back to CSS has data-woom-fallback.
Layering (z-index)
The overlay canvas is fixed over the viewport with z-index: 40 by default: above ordinary content and sticky headers, below typical modals and toasts. When the hovered button sits in a stacking context with a higher z-index (a drawer at 1000, say), the canvas is raised just above it for as long as that button is out. Buttons in the browser's top layer (modal dialogs, popovers, fullscreen) sit above any z-index, so while one is out the canvas joins the top layer too, as a popover, and leaves it once nothing there is out. In browsers without popovers those buttons use the CSS fallback.
How it works
- On page load you pay for a few delegated listeners and every variant's name: 3 KB gzipped. No canvas, no WebGL context.
- The first mouse over the page, once the browser is idle, or the first hover on a woooms button, fetches the renderer (about 20 KB gzipped) and its variant, unless it's pop or keycap (1–4.5 KB), waits for
document.fonts.readyand creates one shared canvas. - The button is measured and painted into a texture: background, every text run at its laid-out position, inline SVGs with their computed styles, same-origin images. A rounded-rectangle block matching its border-radius is extruded behind it.
- Springs (semi-implicit Euler) drive pop, press and pointer follow. The DOM button goes to opacity 0 while its twin is out and is stretched with a CSS transform over the block's footprint, so clicking anywhere on what you see clicks your real element; layout never moves. The shadow is the block's own rounded shape, cast away from the light and softening as the block rises. The frame loop runs only while something moves, and the canvas is hidden when idle.
- The block is rebuilt when the button's size, markup, colours, font or theme variables change, even mid-hover. It's freed as soon as it settles back or the element leaves the DOM.
Why not three.js? The same effect built on three.js weighs 143 KB gzipped. The scene is one extruded shape, one textured face, one lit wall shader and a soft shadow, so a purpose-built WebGL2 renderer does it, both variants included, in about a tenth of the size.
Known limits
- The face copies background colours and gradients (linear, radial, conic), borders, inset rings, text, inline SVG and same-origin images. Background images, text shadows, outer box-shadows and cross-origin images aren't painted; a gradient it can't follow (a calc() position, say) is left out and the colour shows.
- Inline SVG icons are painted through data: URLs: a Content-Security-Policy whose img-src leaves out data: drops them from the face.
- An inline element broken over two lines (a long link) has no one box to lift, and keeps its CSS fallback. So do elements inside a rotated ancestor, and vertical writing modes aren't laid out.
- Buttons with a transparent background get a solid face in the wall colour (glass and jelly draw see-through).
- Content inside other components' shadow roots isn't painted onto the face; elements that are themselves in a shadow root work.
- The block is drawn over everything below the canvas's z-index, including content that scrolls over the button (a sticky header, say) while it's out.
- While it's out, the block takes the clicks for whatever it covers, just as it hides it. Once it settles back, everything underneath is reachable again.
Free, all of it
Every variant is free and MIT. If woooms made your page a little more fun, you can keep it going.