Skip to content
animorio
IT
All components

The elastic button

Buttons, Hover, TextGSAP, CSS onlyEasyCompressed weight: Demo 4.4 KBGSAP + CustomEase 31 KBFreeDownload the zip

Three pill buttons that answer like rubber: under the pointer the label widens on the typeface’s width axis and gets bolder, the fill swells past its size and bounces back, and a press squashes it, wider and lower, before throwing it back on a spring.

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 elastic button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the buttons: the label widens on the typeface's width axis, the fill swells with a bounce, a press squashes it.
         The wide label is already measured by an invisible copy, so nothing around them ever moves -->
    <div class="buttons" data-buttons>
      <a class="elastic elastic--ink" href="#" data-elastic>
        <span class="elastic__fill" aria-hidden="true"></span>
        <span class="elastic__label" data-label="Get started"><span class="elastic__text">Get started</span></span>
        <svg class="elastic__icon elastic__icon--arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h15M13 6l6 6-6 6"/></svg>
      </a>
      <a class="elastic elastic--accent" href="#" data-elastic data-autoclic>
        <span class="elastic__fill" aria-hidden="true"></span>
        <span class="elastic__label" data-label="Read the guide"><span class="elastic__text">Read the guide</span></span>
        <svg class="elastic__icon elastic__icon--page" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3.5h7.5L19 8v12.5H7zM14.5 3.5V8H19M10 12.5h6M10 16h6"/></svg>
      </a>
      <a class="elastic elastic--ghost" href="#" data-elastic>
        <span class="elastic__fill" aria-hidden="true"></span>
        <span class="elastic__label" data-label="Send a gift"><span class="elastic__text">Send a gift</span></span>
        <svg class="elastic__icon elastic__icon--gift" viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 10h15v10h-15zM3.5 7h17v3h-17zM12 7v13M12 7c-1.2-2.6-4.9-3.6-5.4-1.5C6.2 7.2 9.5 7 12 7zm0 0c1.2-2.6 4.9-3.6 5.4-1.5.4 1.7-2.9 1.5-5.4 1.5z"/></svg>
      </a>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/CustomEase.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.

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

What it does

Three pill buttons on a white background: Get started, dark with an arrow; Read the guide, rust with a sheet; Send a gift, with just its outline and a gift box. When the pointer enters a button, the label widens to 122% of its width and goes from weight 620 to 720, on a curve that goes past and comes back; the fill swells by 6% on the same curve and a soft shadow grows under it, as if it lifted off the page. Meanwhile the arrow moves forward 5 pixels, the sheet tilts 10 degrees and the box 8. A press squashes the fill in 0.12 seconds, 4% wider and 10% lower; letting go throws it back to its size on an elastic spring. The button never pushes anything around, because it already keeps the room for the label at its widest. On a phone a tap does the same, and the label stays wide for a moment, so the finger sees it. When they arrive the buttons pop in one after the other with the same bounce.

How it works

The label that widens

Mona Sans is a variable typeface with a width axis, from 75 to 125. Under the pointer the label goes from font-stretch 100% to STRETCH and from weight WEIGHT[0] to WEIGHT[1] with a CSS transition of TIME seconds. The curve is BOUNCE, a cubic-bezier with its second number above 1: it takes the label past its width and brings it back. The script writes these values into CSS variables, so the stylesheet uses them without repeating them.

The room for the wide label

Next to the label sits a copy of it, made with ::before and attr(data-label), already at the widest width and the heaviest weight, zero pixels high and invisible. The two share one grid cell: the button is as wide from the start as it will ever be, and widening moves neither it nor the buttons next to it.

The fill that swells

The fill is a layer of its own behind the label, so it can swell without stretching it. GSAP takes it to INFLATE in TIME seconds on the same curve as the label, made into a CustomEase. While the button is on, a shadow grows under the fill, with a CSS transition.

The press

A press squashes the fill by SQUASH, wider and lower, in 0.12 seconds, starting from the size it has at that moment. Letting go sends it back to its size in 0.9 seconds with an elastic.out of strength and time RELEASE. The tweens use overwrite: 'auto', so a press cleanly interrupts a fill that is still swelling.

Finger and keyboard

The mouse turns the button on when it enters and off when it leaves. A finger has no «over»: a tap turns on and presses at once, and after it lets go the button stays on for PULSE seconds. With the Tab key the button turns on when you reach it (only with :focus-visible, not after a click) and Enter presses it. A click that comes from neither a pointer nor the keyboard, from another script for instance, gets the same answer as a tap.

Values to change

NameFileDefaultWhat it changes
STRETCHscript.js122How wide the label gets, as a percentage of its width (the typeface goes from 75 to 125).
WEIGHTscript.js[620, 720]The label’s weight at rest and when the button is on.
INFLATEscript.js1.06How much the fill swells when the button is on.
TIMEscript.js0.6The seconds of the swelling and the widening.
BOUNCEscript.js[0.34, 1.9, 0.45, 1]The curve of both, as a cubic-bezier: a second number above 1 goes past and comes back.
SQUASHscript.js[1.04, 0.9]The fill under a press: how much wider and how much lower.
RELEASEscript.js[1, 0.35]The spring when a press ends: its strength and how quickly it settles.
PULSEscript.js0.45After a tap with a finger, the seconds the label stays wide.

Try them in the Playground

When to use it

For the main button of a page, or a row of two or three actions in different colours: buy, discover, give. It needs a variable typeface with a width axis (Mona Sans, Roboto Flex, Anybody): with a static typeface the label does not widen and only the fill is left.

On phones

The buttons stack one under the other and are a little lower. A tap turns the button on and presses it at once; the label stays wide for a moment after the tap, then goes back.

With reduced motion

No widening and no swelling: the label stays as it is and the fill does not move. Only the colour answers, a little lighter on the dark button and darker on the rust one, and the shadow below.

Pitfalls

  • The width must stay inside the typeface’s axis: Mona Sans goes from 75 to 125, and beyond that the browser stops at the last value. With another typeface STRETCH must change too.
  • Without the invisible copy, widening the label widens the button, and in a row of centred buttons the others shift too every time the pointer passes.
  • The fill must swell on its own: scaling the whole button would scale the label too, which is already widening by itself. That is why the fill is a separate layer, with z-index −1 and isolation on the button.
  • On a button tapped with a finger, being on lasts as long as the press, a moment: the PULSE delay is there to show the wide label.

Libraries

GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 4.4 KB; the libraries: GSAP + CustomEase 31 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