v0.2

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.

Pictures too: woooms/shapes makes any cut-out picture of something, a balloon, a gummy, a gold statue of it. See Pictures.

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"

Arcade

A real push button: a glossy domed cap in a collar, with a hard click.

data-woom="arcade"

Pocket pet

A 90s virtual pet: it hatches, follows the pointer and jumps for joy when pressed.

data-woom="pet"

Coin

Minted in metal: flips to a check, spins while it loads, or just flies.

data-woom="coin"

Receipt

A thermal printer: a receipt prints with your label as the shop's name, torn on press.

data-woom="receipt"

Holographic

A holo foil card: colours, stars and glare follow the pointer.

data-woom="holographic"

Defuse

A planted bomb, beeping faster: hold to defuse it.

data-woom="defuse"

Grass block

A blocky game's grass block: it tilts to show its top; a press mines it into cubes.

data-woom="grass"

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.

<Woom variant="keycap" sound>
    <button>Enter</button>
  </Woom>

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.

import { Woom } from "@woooms/react";

  <Woom variant="pop" burst="hearts">
    <button>Like</button>
  </Woom>

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.

Pictures, made of things

Any picture can come out in 3D too, made of something: a cut-out photo, a logo, an icon, an emoji. Its outline is read from its transparency, so it comes out as its own shape, blown up off the page, and the picture turns into its material: a balloon of it, a gummy of it, a gold statue of it. Each is played with as its material is.

Holographic foil over the print, the rainbow sliding as you move.

Material
import { Shape } from "@woooms/react/shapes";

  <Shape material="balloon" src="/me.png" alt="Me" height={200} />

Materials

Fourteen, each the Studio's own. Some turn the picture into themselves (a statue, a gummy in your colour), some lay themselves over it (foil, a print); each is lit by a lamp that follows the pointer.

Balloon

balloon. Hold one still to pop it; press it again to blow it back up.

Gummy

gummy. Press one to make it hop; run the pointer across it to set it wobbling.

Clay

clay. Press it to push a fingertip in; hold to push deeper. It keeps your dents.

Chocolate

chocolate. Press it to take a bite.

Wax

wax. Hold the pointer over it: the pointer's a hairdryer.

Ice

ice. Press it to crack it; the third blow breaks it. Press again to mend it.

Chrome

chrome. A statue of it in polished silver, the room mirrored in it.

Gold

gold. A statue of it, cast in gold.

Marble

marble. A bust of it, carved.

Holo

holo. Holographic foil over the print, the rainbow sliding as you move.

Cut out

cutout. Cut out of card, a border of it left round the print.

Thermal

thermal. Seen through a thermal camera.

One-bit

onebit. Printed in one ink, dot by dot.

Halftone

halftone. Printed in halftone dots.

Picture options

As options to shape() and props of <Shape>, or as data-woom-* attributes on the picture for autoShapes().

OptionAttributeDefaultWhat it does
materialdata-woom-shape"balloon"What it's made of: any of the materials above.
colordata-woom-colorthe material'sThe colour gummy, wax, one-bit and halftone turn the picture, and a balloon if it's given one. Any CSS colour.
depthdata-woom-depththe material'sHow deep it is, as a share of its height.
puffdata-woom-puffthe material'sHow round: 0 flat on top with rounded edges, 1 round all over.
outdata-woom-outtrueOut all the time; false brings it out only while the pointer's over it.
playdata-woom-playtrueWhether a press does what its material does (pops, bites, cracks…).
swaydata-woom-sway0How much it turns to and fro on its own, 0..1, as if on show.
sounddata-woom-soundfalseIts material's sounds: a pop, a crunch, a crack.
lean, tilt0.1, 0.5How far it leans back, and how much it turns to follow the pointer (radians).
onPlayCalled with what happened ("pop", "blow-up", "hop", "dent", "bite", "crack", "break", "mend") and the picture.

shape() returns a function that undoes it; calling it again for the same picture starts it over. The <img> stays yours: its alt text, its clicks, its layout.

Moments: a paper dart

woooms/fx is for the moments an interface has: something sent, something done. One call, and the element itself, as it is on the page, is folded into a paper dart, crease by crease, and thrown: off the page, or into another element. Press Send.

To Ana

Lunch on Friday?

That ramen place opened round the corner. Half twelve? I'll book if you're in.

Outbox
Folding
import { fly } from "woooms/fx";

  function Draft() {
    const outbox = useRef<HTMLDivElement>(null);
    return (
      <>
        <form onSubmit={(e) => {
          e.preventDefault();
          fly(e.currentTarget, { to: outbox.current!, back: true });
        }}>…</form>
        <div ref={outbox}>Outbox</div>
      </>
    );
  }

Landing

land() is fly() the other way: a paper dart folded from the element glides in from off the page, banking into its curve, flares and touches down where the element is, turns to lie as it does, and unfolds into it, crease by crease. For whatever's new: a message, a notification, a row that's just come in.

  • AnaLunch on Friday? I'll book.
import { land } from "woooms/fx";

  // Before it's painted, so it's never seen there till the dart has landed.
  useLayoutEffect(() => {
    land(newest.current!, { sound: true });
  }, [messages.length]);

Printer

print() brings the element in printed: a receipt printer pops up where its bottom edge is and prints it, the paper coming up out of the slot top edge first, a line at a time, the ink coming up on it as it passes the head, curling towards you as receipt paper does. Then it's cut with a little tug, settles flat into its place, and the printer goes. For receipts, tickets, anything that's just been made for you.

Corner Café

Order #1041 · Till 2

Flat white
3.20
Almond croissant
2.80
Oat milk
0.40

Total£6.40

Thank you!

import { print } from "woooms/fx";

  // Mounted out of sight; printed in as it's shown.
  useLayoutEffect(() => {
    print(receipt.current!, { sound: true });
  }, []);

Scratch card

scratch() covers the element in a scratch card's coating, wiped on with a glint: matte silver latex, stars printed on it, glitter in it that catches the light as the pointer moves. Wherever the pointer goes the coating comes away, its edge crumbly, leaving crumbs of latex lying about as a coin does; once enough's off (reveal, half and a bit by default), the rest wears away and it resolves. Pick the coating's colour with foil. Without 3D, it's a canvas you rub off, with a finger too.

You won

20% off

Code SUMMER20

Foil
import { scratch } from "woooms/fx";

  useEffect(() => {
    scratch(card.current!, { sound: true }).then(() => claim(prize));
  }, [prize]);

Rolled up

roll() rolls the element up as a poster on a table is: from an edge (from, the bottom by default) it lifts and rolls up on itself round a spiral, picture in, the paper's back on the outside, then the roll carries on rolling, off the page. unroll() is the same backwards: a roll comes rolling in and lays the element out flat.

Rooftop cinema

Summer Film Nights

Every Friday · July & August · 21:00

Rolled from
import { roll, unroll } from "woooms/fx";

  <button onClick={async () => {
    await roll(poster.current!, { sound: true });
    archive(poster.id);
  }}>Archive</button>

Page turn

turn() turns the element over like the page of a book: hinged along one edge, it's lifted by the other (from, the right by default), its corner first, and turns over through the air, bowed as a page is, to lie down on the other side, its back up, and fade away; what's under it shows as it goes. For steps, onboarding, a gallery.

Step 1 of 3

Pick a variant

Jelly, glass, a coin: whatever your button's made of.

Turned by
import { turn } from "woooms/fx";

  <button onClick={async () => {
    await turn(steps.current[at]!, { sound: true });
    setAt(at + 1);
  }}>Next</button>

Flip

flip() turns an element over like a card to show another: lifted and turned, faster and faster, till it's edge on, where the other side (an element laid out in the same place, hidden till then) takes over, turning on from edge on and set down with a little bounce. Its edge shows as it turns; its shadow goes with it. Without a back, it's turned right round to itself.

Turned
import { flip } from "woooms/fx";

  <div style={{ position: "relative" }} onClick={() => flip(question.current!, answer.current!)}>
    <div ref={question}>What's the capital of Australia?</div>
    <div ref={answer} style={{ position: "absolute", inset: 0, visibility: "hidden" }}>
      Canberra.
    </div>
  </div>

Origami

origami() folds the element into a model, a step at a time, as hands would: squared up first (whatever's past a square folded behind it), then folded, creased and shaped. A crane flaps its wings and flies away; a frog crouches, hops, and leaps off head over heels; a boat sails away, rocking; a fortune teller chomps three times and spins off. model says which, and to sends it into something.

A note for you

Thank you for coming. Make a wish.

Into
import { origami } from "woooms/fx";

  <button onClick={() => origami(note.current!, { model: "crane" })}>Fold it</button>

Crumpled paper

crumple() crushes the element into a ball of paper in two hands. It's simulated: a sheet that bends but never stretches, so it creases where it gives, with the element still printed on it, folded up in the ball. Then it's lobbed into the bin, or let go of, to bounce and roll off the page. Tick a note off.

  • Call the printer back
  • Order more oat milk
  • Fix the login bug
Bin
import { crumple } from "woooms/fx";

  function Note({ text, onDone }: { text: string; onDone: () => void }) {
    return (
      <li>
        {text}
        <button onClick={async (e) => {
          await crumple(e.currentTarget.closest("li")!, { to: document.querySelector("#bin")! });
          onDone(); // gone: take it out of the list
        }}>Done</button>
      </li>
    );
  }

Sticker

peel() peels the element off the page like a sticker: a corner lifted, then the line where it's still stuck drawn across it, the peeled part rolled back over itself, its white back up, the glue giving in fits and starts. What's under it on the page shows as it comes away. Free, it's pulled off the page, or turned face up and stuck onto another element.

20% off

Your codeWOOOMS20

Wallet
import { peel } from "woooms/fx";

  <div className="coupon">
    <code>WOOOMS20</code>
    {/* Over the code: peeled off, the code shows. */}
    <button className="sticker" onClick={(e) => peel(e.currentTarget)}>Peel me</button>
  </div>

Torn in two

tear() tears the element in two, from its top edge down along a ragged line: the halves open from the tear as it runs, one edge showing the paper's white core with its fibres sticking out. Let go of, they fall off the page, turning over as they go; or, given to, they're tossed into it.

woooms live

The Roundhouse · Fri 14 Nov · 20:00

Row
F
Seat
12
import { tear } from "woooms/fx";

  <button onClick={async () => {
    await tear(ticket.current!, { sound: true });
    cancel(order.id);
  }}>Cancel ticket</button>

Shredder

shred() feeds the element through a shredder that pops up under it: it takes hold with a jolt, the motor hums and grinds, and the element comes out of the bottom in strips, still printed, curling as cut strips do; once it's all through they drop away and the shredder goes.

Northern Bank Confidential

Account •••• 4821

Balance $12,480.07

Statement October

import { shred } from "woooms/fx";

  <button onClick={async () => {
    await shred(statement.current!, { sound: true });
    remove(statement.id);
  }}>Shred it</button>

Fire

burn() sets the element alight: a spark at its bottom edge, a little flame, and then it catches. A ragged front climbs it, faster up than across as fire does: ahead of it the paper browns, at it a line glows and flickers, flames lick up off it, and behind it the paper chars black, curls and falls away to ash that drifts up with the smoke. What's under it on the page shows through the hole. Give it at, a point or the click, and it's lit there: click anywhere on the note.

Top secret

The launch moves to Tuesday. Tell no one. Not even the cat.

— M.

import { burn } from "woooms/fx";

  <button onClick={async () => {
    await burn(message.current!, { sound: true });
    onDelete(); // gone for good
  }}>Burn after reading</button>

Dust

dust() turns the element to dust: it trembles, then a ragged front sweeps across it and, wherever it's passed, the element comes apart into thousands of specks of itself that the wind carries off, eddying and turning to ash. wind says which way it blows, "left" or "right".

Peter Parker

Photographer · Daily Bugle

Wind
import { dust } from "woooms/fx";

  <button onClick={async () => {
    await dust(card.current!, { sound: true });
    removeMember(member.id);
  }}>Remove from team</button>

Vortex

vortex() whirls the element away. It twists, its middle turning faster than its edge, then comes apart into specks of itself: down a drain (a hole opens in its middle and the rest pours into it, winding into spiral arms, or into to), or, with kind: "tornado", up a column that widens as they climb.

Downloads

3.2 GB

142 files you haven't opened in a year

Kind
import { vortex } from "woooms/fx";

  <button onClick={() => vortex(files.current!)}>Clear it out</button>

Unravelled

unravel() knits the element, its picture worked in stitches, then pulls its yarn from the top: it comes undone a row at a time, along one and back along the next, as knitting does, the yarn going off up the page crimped and in the colours of the stitches it was, till its tail whips away. to pulls it into something.

Winter knits

30% off

Jumpers, scarves and hats. Ends Sunday.

import { unravel } from "woooms/fx";

  <button onClick={() => unravel(sale.current!)}>End the sale</button>

Shattered glass

shatter() breaks the element like a pane of glass, where it's hit: cracks race out from the blow, radial and in rings, and the picture goes askew from shard to shard; a beat, then the shards fall out, nearest the blow first, tumbling off the page and flashing as they turn, splinters spraying. Pass the click as at and it breaks where it was clicked; for a see-through pane, pass clear and its shards are as clear as it was.

import { shatter } from "woooms/fx";

  <div ref={pane} onClick={async (event) => {
    await shatter(pane.current!, { at: event, sound: true });
    setBroken(true);
  }}>The glass ceiling</div>

Genie

genie() sucks the element into another, as a window's minimised into the dock: its near edge is drawn out to it, the element stretched behind it in a funnel that curves into it, then the rest pours down after, slowly and then all at once, and it's gone in, which catches it with a little dip. For whatever's added to a cart (a product's photo, say), saved to a folder or put away.

Kiln & Co.
Cart

Speckled mug

Stoneware · 350 ml

£18

Into
import { genie } from "woooms/fx";

  <button onClick={async () => {
    await genie(product.current!, { to: cart.current!, back: true, sound: true });
    addToCart(item);
  }}>Add to cart</button>

Melt

melt() melts the element away: it goes soft and sags, then melts down, its top sagging in lobes, into a pool along its bottom edge that spreads out either side, drips running off it; then what's left lets go and runs on down the page after them, thinning away as it goes. Its own picture runs with it, text and all. For whatever's run out: an offer that's over, a deadline that's passed.

Summer sale

−40%

on everything, while the sun's out

import { melt } from "woooms/fx";

  useEffect(() => {
    if (expired) melt(offer.current!, { sound: true }).then(onGone);
  }, [expired]);

Ice

freeze() ices the element over, and keeps it so: frost creeps in from its edges in ferns, the ice thickens over it, and it's seen through the ice, cold and a little bent, out of reach (no pointer, aria-disabled) till thaw(), when the ice goes wet, drops run down it and it melts away. kind: "block" sets it in a deep block of ice instead, turned a little to show it. For whatever's paused, locked or on hold: a frozen card, a paused plan.

Active

Ice
import { freeze, thaw } from "woooms/fx";

  useEffect(() => {
    const card = ref.current!;
    if (frozen) freeze(card, { sound: true });
    else thaw(card);
  }, [frozen]);

Moment options

fly(), crumple(), peel(), tear(), shred(), burn(), dust(), shatter(), genie() and melt() each return a promise that resolves once it's gone (or, with back, once it's back), the element left hidden for you to remove. Called again while it's going, it's the same moment.

OptionDefaultWhat it does
tooff the pageWhere it's thrown: an element (it goes into it, and the element takes it in with a little gulp) or a point { x, y } in client px. Left out, a dart flies up and away; a ball is let go of, to bounce and roll off the page; a sticker is pulled off it; torn halves fall off it; a genie goes down into the bottom of the window.
backfalseBrings the element back once it's gone, set down in its place, rather than leaving it hidden.
folding"steps"fly() only. How it's folded: "steps", a crease at a time, as hands fold one; or "pairs", both sides at once.
soundfalseThe paper: creased, crushed, thrown, bouncing, landing in the bin; the glue letting go.

The paper is cut from the element's own picture, so it's your button, your card, your draft that's folded, crumpled, peeled, torn, shredded, burnt, broken, sucked away or melted; the paper's back is its background colour. woooms/fx is 6 KB; each moment's own code (1.4 to 6 KB) is fetched the first time it's played, or ahead with prepare(). shred() and melt() take only back and sound; shatter() and burn() take at too, a point or the click that broke it or lit it; dust() takes wind, "left" or "right"; vortex() takes kind and to; origami(), model and to. freeze() keeps it so: it resolves once it's frozen through, and thaw() once it's melted away.

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, images it may read (as object-fit places them). 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 images from the same site or served with CORS. Background images, text shadows, outer box-shadows and other sites' 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