A delete button that doesn't just say delete, it does it. Press it and the trash bin tips open, every letter of the label hops out and falls in, the pill squeezes into a disc, a ring spins while your request runs, then seals. If the request fails, the timeline rewinds: the bin spits the letters back, one by one.
One HTML file. No framework. No build step. No dependencies.
| π€ Letters are the progress meter | Every landing raises the bin's level. The animation is the state, not a timer. |
| π One clip, pill to disc | The face and the hit target share one clip-path, so what you see is what you can click. |
| βͺ Failure = rewind | Errors play the eat in reverse: letters pop back out and the label rebuilds. |
| π΅ Musical sound | WebAudio synth, no files. Pitch climbs a pentatonic scale as the bin fills. Starts only after a click. |
| π³ Haptics | Tiny vibration ticks on supported mobile browsers. |
| π§² Magnetic tilt | The button leans toward your pointer. |
| β¨ Seal burst | Colorful confetti shards on success. |
| βΏ Accessible | aria-busy, a live role="status" region, visible keyboard focus, prefers-reduced-motion support. |
git clone https://github.com/wearepixelflow/Hungry-Delete-Button.git
cd Hungry Delete Button
open index.html # or just double-click itThat's it. Everything (markup, CSS, JS) lives in index.html. The only external request is the Google Fonts link for the title, which falls back to a system font offline.
- Copy the
.gulprules from the<style>block. - Copy the
<template id="tpl">block (the button markup). - Copy the
<script>block. - Drop a placeholder where you want a button:
<span class="gulp" data-state="idle"></span> <!-- ink + lime -->
<span class="gulp" data-state="idle" data-tone="blue"></span> <!-- electric blue -->Each placeholder is filled from the template when the script runs.
The demo fakes the server with a wait. In the click handler, find this line:
await sleep(quick ? 150 : 1100);and replace it with your request, e.g.:
const res = await fetch('/api/items/42', { method: 'DELETE' });
if (!res.ok) throw new Error('delete failed');Heads up: the demo's "fails" button is hard-wired to fail through the
failoption passed toGulp(). For a real button, branch on your request result instead: success runs the seal path, an error runs the rewind path (both already exist in the handler).
Want a different word? Change the text inside <span data-label>Delete</span> in the template. Every letter becomes its own animated element automatically.
Everything is driven by CSS custom properties on .gulp:
| Variable | What it does | Default (ink) |
|---|---|---|
--fg |
Label, bin and fill color | #c8ff3a |
--face |
Button background (any gradient) | dark ink gradient |
--ring |
Focus / accent color | #c8ff3a |
--sh |
RGB triplet for the drop shadow | 20,20,26 |
--rr |
Corner radius of the pill | 48px |
--t |
Pill β disc morph duration | .44s |
Make a new flavor in four lines:
.gulp[data-tone="hot"]{
--fg:#fff; --ring:#ffd1e6; --sh:255,61,139;
--face:linear-gradient(120deg,#ff8a1f,#ff3d8b 60%,#8b5cff);
}<span class="gulp" data-state="idle" data-tone="hot"></span>The button is a tiny state machine driven by data-state on the root element. CSS owns how each state looks; a short async function owns when it changes.
| State | What happens |
|---|---|
idle |
Pill at rest. Pointer tilt active. |
eat |
Lid pops, bin tips, each letter flies on a Web Animations arc into the bin. Each finished flight raises --p (the fill level) and plays a note. |
furl |
Pill clips down to a disc, the bin slides to the center. |
pending |
An arc spins around the disc for as long as your request takes (with a minimum so it never flickers). |
done |
The ring seals, the bin empties, confetti bursts. |
back |
The same clip runs backwards to the pill and the label returns. On failure, letters are spat back in reverse order first. |
Nerdy bits worth stealing:
@property --pand--fxare registered, so the browser interpolates them natively, with norequestAnimationFrameloop.- Letter flights use the Web Animations API, and
animation.finishedis the progress signal. - The bin's inside is a
clipPath, so liquid is only ever drawn inside the bin. - Failure is
playbackRate = -2.4on the very same letter animations.
Built for current evergreen browsers (Chrome, Edge, Safari, Firefox). It relies on registered custom properties (@property), :has(), individual transform properties (translate, rotate, scale) and the Web Animations API. Older browsers may lose parts of the effect.
hungry-delete/
βββ index.html the whole component + demo page
βββ assets/
β βββ banner.svg animated README banner
β βββ demo-success.svg animated success flow
β βββ demo-fail.svg animated failure flow
βββ LICENSE
βββ README.md
- Extract into a package with
Gulp(el, { request }) - Press-and-hold to confirm
- Real letter physics (Matter.js) in the bin
- More verbs: Send, Shred, Pay
- React / Vue / Svelte wrappers
Ideas, new tones and new "eating" animations are welcome. Open an issue or PR.
MIT Β© YOUR NAME
Made with ποΈ and a questionable appetite.