v0.1

Make yours
go woooom.

Your buttons, made of something: real 3D, still your own element. 3 KB until hover.

On this page

Quick start

$ npm i woooms
import { 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.

OptionTypeDefaultWhat it does
variant"pop" | "keycap" | registered name"pop"Which effect. Picks that variant's defaults for everything below.
kindstringthe variant's firstFor variants that come in kinds: which one (a coin: "flip", "spin" or "toss").
depthnumber0.8 / 0.45Block depth as a fraction of the button's height.
leannumber0.42 / 0.14How far the face leans back when fully out, in radians.
tiltnumber0.2 / 0.06How much the block turns to follow the pointer, in radians.
tapernumber0.14 / −0.1Positive: the face grows (+14%). Negative: the base flares out instead and the face keeps the button's size.
linesbooleantrueSpeed lines running along the walls (pop has them; keycap only if you ask).
burststringWhat a press throws, on any variant: confetti, sparks, hearts or bubbles. See Bursts.
soundbooleanfalseThe variant's sound. Keycap: a clack when it bottoms out and when it comes back.
depthColorCSS colourbackground, darkenedWall colour.
lineColorCSS colourdeeper wall colourSpeed line colour.
shadowTintCSS colournear-black + wallShadow colour.
reducedMotion"user" | "always" | "never""user"Override the motion preference for this button.
outbooleanfalseKeep 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) => voidCalled when the block starts popping out.
onSink(el) => voidCalled 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-colorWall colour
--woom-linesSpeed line colour, or none to turn them off
--woom-shadow-tintShadow colour
--woom-depthSame as the depth option
--woom-leanSame as the lean option
--woom-tiltSame as the tilt option
--woom-taperSame 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 cost

Accessibility

  • The effect is decoration only. Nothing it shows carries meaning that the button itself doesn't.
  • The canvas is aria-hidden and pointer-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.
  • disabled and aria-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:

WhereWhat you getNotes
Desktop Chrome / Edge, mouseWebGL effectTested (Chromium, CI with SwiftShader and on a real GPU)
Desktop Firefox, mouseWebGL effectExpected; not yet verified
Desktop Safari 16.4+, mouseWebGL effectExpected; not yet verified
Phones, tablets, any touch inputYour button as isNo hover on touch, so nothing to pop
prefers-reduced-motion: reduceYour button as isNeither WebGL nor CSS motion
No WebGL2 / renderer fails to startCSS fallbackTested
Button inside a modal <dialog> or popoverWebGL effectThe canvas joins the top layer while it's out (popovers needed; otherwise CSS fallback)
WebGL context lostButton restored at once, 3D on next hoverGives 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

  1. On page load you pay for a few delegated listeners and every variant's name: 3 KB gzipped. No canvas, no WebGL context.
  2. 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.ready and creates one shared canvas.
  3. 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.
  4. 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.
  5. 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.

Hayder, who makes wooomsSigned, Hayder