Skip to content
animorio
IT
All components

The soap bubble button

Buttons, Hover, PhysicsGSAPMediumCompressed weight: Demo 5.5 KBGSAP 28 KBFreeDownload the zip

A pill of soap with colours swimming in the film: it breathes, leans towards the pointer, and pops when you press it. A hole opens from the point you touched and runs across the film, droplets fly off the rim, the words become “Invite sent” with a tick and, a moment later, the bubble blows back up with a bounce.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The soap bubble button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the soap bubble button: a pill of soap film with slow colours swimming in it. It breathes, leans towards the
         pointer, and when it is pressed it pops: a hole opens from the point you touched and runs across the film,
         droplets fly off the rim, the words become "Invite sent" and, a moment later, the bubble blows back up -->
    <div class="soap" data-soap>
      <button class="soap__btn" type="button" aria-label="Send invite" data-btn data-autoclic>
        <span class="soap__film" aria-hidden="true" data-film>
          <span class="soap__swirl" data-swirl></span>
          <span class="soap__shine"></span>
        </span>
        <span class="soap__outline" aria-hidden="true" data-outline></span>
        <span class="soap__label">
          <span class="soap__word" aria-hidden="true" data-word>Send invite</span>
          <span class="soap__word soap__word--done" aria-hidden="true" data-done>
            <svg class="soap__tick" viewBox="0 0 20 20"><path data-tick d="M4.5 10.5l3.6 3.6 7.4-8.2"/></svg>
            <span data-done-text>Invite sent</span>
          </span>
        </span>
      </button>
      <span class="soap__drops" aria-hidden="true" data-drops></span>
      <p class="sr" aria-live="polite" data-live></p>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

19 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The soap bubble button” from Animorio and fit it into my site

What it does

On a white background a pill 60 pixels tall made of soap film: pink, light blue, pale green and yellow swim slowly inside it, a white shine sits on its upper edge and a violet hairline runs all round. The pill breathes, never repeating exactly. Under the mouse it leans towards the pointer on a soft spring and the colours turn faster; pressing squashes it. On the click it pops: from the exact point you touched a hole opens and runs across the film until it is gone, twenty-six droplets with a shine leave the rim (the ones nearest the hole first), the words roll up into “Invite sent” with a tick that draws itself, and only a pale outline is left where the film was. After two and a half seconds the bubble blows up again from a speck, with a bounce, and the words come back. With the keyboard it pops from the middle.

How it works

The film

A pill with hidden overflow that holds a big conic gradient (the script writes it from TINTS), blurred by 12 pixels and turned slowly by GSAP. On top sit an oval white veil that clears the middle so the words read well, and two shines; the pink-under, blue-over rim is an inset box-shadow on a separate layer above the colours.

The breath and the spring

Two endless tweens of different lengths (scale one way, half a degree of rotation the other) so the shape never repeats. The whole button follows the pointer with three quickTo (x, y, rotation) on elastic.out; pressing scales it 1.035 across and 0.9 down, and on release it comes back with an elastic.

The hole

The film’s mask is a radial-gradient, transparent inside and black outside, centred on the click. A tween on a plain object grows its radius from 0 to the distance of the farthest corner, with power2.in, and on every frame the script rewrites the mask. When the hole is done the film goes to opacity 0 and the mask is removed.

The droplets

Each starts on a point of the pill’s elliptical edge and leaves along the normal, which on a wide pill points mostly up and down. One tween throws it and a second makes it fall and fade. The delay is the distance from the click divided by the width, so the first to leave are the ones the hole reaches first. Each has a radial gradient with a white spot and a ring of the same colour.

The states

At rest, popped, blowing up. The two lines of text sit in the same cell of a grid, so the button always has the same width; one rolls up and out, the other comes in from below with back.out. The tick is an SVG path drawn with strokeDashoffset. While popped the button has aria-disabled and an aria-live region says “Invite sent”.

Values to change

NameFileDefaultWhat it changes
LABELscript.js'Send invite'The words at rest.
DONEscript.js'Invite sent'The words after the pop.
DROPSscript.js26How many droplets fly off.
REACHscript.js70How far the droplets fly, in pixels.
HOLDscript.js2.4The seconds it stays popped before blowing back up.
BREATHscript.js[0.02, 4.5]How much it swells and shrinks, and the seconds of one breath.
POPscript.js0.22The seconds the hole takes to run across the film.
TINTSscript.js['#ff7ac8', '#7ad0ff', '#b8ff8f', '#ffd978']The colours of the film, in turn.

Try them in the Playground

When to use it

For an action you do once and watch happen: invite, send, unlock, redeem. Where the tone is light and the confirmation wants a small gesture of play.

On phones

Taps work like clicks and the hole starts from the point touched. With a finger there is no leaning towards the pointer and no faster colours; everything else is there.

With reduced motion

No breathing, the colours still, no leaning or squashing. The film fades with a short dissolve, with no hole and no droplets; the words swap with a fade and the bubble comes back with a fade.

Pitfalls

  • The hole’s mask is centred in the film’s pixels: if the film is enlarged by the breath the offset is a pixel or two, but the mask cannot be animated with a CSS variable without @property, so the script rewrites it on every frame.
  • A mask on the film also cuts its outer shadow: it must only be set while it pops and removed right after, not left on.
  • The two lines of text must sit in the same cell of a grid: if one leaves the flow, the button changes width between the two states and the edge jumps.
  • Blowing up, the scale starts at 0.15, not 0: from 0 the elastic bounce has nothing to take a run-up from and the bubble just appears.
  • A keyboard click has position 0,0: it must be recognised (detail 0) and made to pop from the middle, otherwise the hole starts from the corner.

Libraries

GSAP 3.15.0, Geist (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 5.5 KB; the libraries: GSAP 28 KB.

Testing

Tested on 12 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components