Skip to content
animorio
IT
All components

The send button

Buttons, Transitions, SVGGSAPMediumCompressed weight: Demo 7.2 KBGSAP 28 KBFreeDownload the zip

The button of a signup that goes through every state of a send: on a press the label leaves and the paper plane takes off, the pill shrinks into a circle with a spinning ring, then a green tick that draws itself, or a red cross and a shake of the head, and a pill again.

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 send button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/dm-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <div class="signup-box" data-box>
      <!-- the form: the button goes through its states (sending, sent, failed) and says each one out loud too -->
      <form class="signup" data-form novalidate>
        <label class="field">
          <span class="sr">Your email address</span>
          <input class="field__input" type="email" name="email" value="ada@example.com" autocomplete="email" placeholder="you@example.com" aria-describedby="stato" data-email required>
        </label>
        <div class="send-slot" data-slot>
          <button class="send" type="submit" data-send data-autoclic>
            <span class="send__halo" data-halo aria-hidden="true"></span>
            <span class="send__label" data-label>Subscribe</span>
            <svg class="send__plane" data-plane viewBox="0 0 24 24" aria-hidden="true"><path d="M21 3 10.5 13.5M21 3l-6.5 18-4-7.5L3 10z"/></svg>
            <svg class="send__state" data-state viewBox="0 0 48 48" aria-hidden="true">
              <circle class="send__track" data-track cx="24" cy="24" r="17"/>
              <circle class="send__arc" data-arc cx="24" cy="24" r="17"/>
              <path class="send__tick" data-tick d="M15.5 24.5l6 6 11-12.5"/>
              <path class="send__cross" data-cross d="M17.5 17.5l13 13M30.5 17.5l-13 13"/>
            </svg>
          </button>
        </div>
      </form>
      <p class="status" id="stato" role="status" aria-live="polite" data-status>No spam. Leave with one click.</p>

      <!-- for the demo: the next send fails, to see the other answer -->
      <label class="fail">
        <input type="checkbox" data-fail>
        <span class="fail__box" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-7"/></svg></span>
        <span>Make the next send fail</span>
      </label>
    </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.

18 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 send button” from Animorio and fit it into my site

What it does

A signup form on a white background: a field with the address and the Subscribe button with a paper plane. On a press the button squashes and springs back, the label rises and vanishes, and the plane crouches, turns its nose to the right, takes its run and climbs more and more, until it leaves the button and fades. Meanwhile the pill shrinks into a circle and a spinning ring appears inside, its arc growing and shrinking. The send, a fake one, takes 1.4 seconds; when the answer comes the ring closes. On a yes the circle turns green, a tick draws itself, the button swells for a moment and a green ring leaves it; on a no it turns red, a cross draws itself one stroke at a time and the button shakes its head. After 2.4 seconds it is a pill again, with «Subscribe» or with «Try again», and a new plane lands in its place. Every state can also be read in the line under the field. An address without an @ or without a dot never leaves: the field shakes, turns red and says what is missing. The «Make the next send fail» box makes the next send fail, then unticks itself. Under the mouse the plane leans forward.

How it works

The fixed slot

The button sits in a slot as wide as its widest pill: once the fonts have loaded the script measures it with every word the button will say (Subscribe, Try again, Sending, Sent, Not sent) and writes the width into a CSS variable. So while the pill shrinks into a circle and opens again with another word, the field next to it never moves. On a phone the button is as wide as the form and there is nothing to measure.

The take-off

The label rises 18 pixels and vanishes in 0.26 seconds. The plane crouches and turns its nose by 32 degrees in 0.14 seconds, then flies for FLIGHT seconds with three tweens at once: 112 pixels right with power2.in, 84 up with power4.in, and the rotation to −14 degrees as it shrinks. The two different curves make the path: the climb starts later and grows faster, so first comes the run and then the flight.

From pill to circle

The button’s width goes to its height in SHRINK seconds with power3.inOut, and with corners rounded at 999px the pill becomes a circle. Inside there is an SVG with two circles, the track and the arc: the arc is a single dash, it turns with an endless tween and meanwhile its stroke-dashoffset goes back and forth, so it grows and shrinks.

The answer

The fake send, fakeSend, is a Promise that resolves after LOADING seconds; in a real form it becomes the fetch to the newsletter service. The mark waits for both the answer and the end of the circle, with Promise.all. The arc closes as its stroke-dashoffset goes to zero, the colour changes and the tick, or the cross, draws itself: it is a dash as long as the line (getTotalLength), pushed out of sight and brought back to zero. A yes swells the button to POP and sends out a green ring; a no shakes it with SHAKE, swings each smaller than the one before.

The way back

After HOLD seconds the mark goes and the pill opens again. The script puts in the new word, reads the button’s natural width with no width set, puts it back and animates to it; at the end it removes it (clearProps), so the button follows the CSS again. The word rises into place and a new plane arrives from the bottom left.

The words

Every state is also said in words, in a paragraph with role="status" that screen readers read by themselves. While sending, the button has aria-disabled and a label with the state, the field is read-only and a second click starts nothing. A wrong address gets aria-invalid, the message says what is missing and goes back to normal as soon as you type.

Values to change

NameFileDefaultWhat it changes
LOADINGscript.js1.4The seconds the fake send takes (a real one takes what it takes).
SHRINKscript.js0.5The seconds of the pill turning into a circle, and back.
HOLDscript.js2.4The seconds the answer stays on the button before it becomes a pill again.
SHAKEscript.js[10, 3]The shake of a no: how far it goes, in pixels, and how many times.
FLIGHTscript.js0.8The seconds of the paper plane’s flight.
POPscript.js1.12How much the button swells for a moment on a yes.

Try them in the Playground

When to use it

For a form with a single action that involves a wait: subscribing, sending a message, saving, booking. It works well when the answer comes in a second or two; for longer waits you need a bar that tells how far along it is.

On phones

The field and the button stack one under the other and the button is as wide as the form: the wide pill shrinks into a circle in the middle and then opens back out to the edges. The rest is the same, and a tap is the press.

With reduced motion

The button does not change shape and nothing flies or shakes: the label changes with a fade (Sending, Sent or Not sent), the fill turns green or red and then dark again, and the line under the field says every state as always.

Pitfalls

  • The mark must be drawn once the answer has come and the circle is done, not before: with a very fast answer, without Promise.all the tick would appear inside a button still halfway between pill and circle.
  • The send starts from the button’s click, not from the form’s submit event: inside an iframe sandboxed without allow-forms (a playground, an online editor) the form is never submitted and the submit never comes, while the click does, including the one the browser fires when you press Enter in the field.
  • The label still says «Subscribe» even when it is invisible, and screen readers would still read it: that is why the button gets an aria-label with the real state while sending.
  • A button turned off with disabled loses focus, and keyboard users end up who knows where on the page: here aria-disabled is used, and a new send is ignored until the first one is over.
  • The animated width must be removed at the end with clearProps: otherwise the button stays a fixed number of pixels wide and no longer follows the CSS when the window changes.

Libraries

GSAP 3.15.0, DM Sans (font variabile) 5.3.0.

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

Testing

Tested on 2 October 2026

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

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