Skip to content
animorio
IT
All components

The button that recomposes the word

Buttons, Text, PhysicsGSAP, CSS onlyAdvancedCompressed weight: Demo 8.8 KBGSAP 28 KBFreeDownload the zip

The Add to bag button of a shop, in the minimal style: a black pill with its letters set like movable type. On a click the word recomposes itself into the next one letter by letter: the letters they share stay and slide to their place (the g of bag runs all the way to the end of Adding), the others leave and drop in one after the other, while the typeface’s width axis keeps every word near the same measure and the pill takes the rest on a spring. Then Added to bag, with the tick drawn in vermilion.

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 button that recomposes the word</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 frame keeps the room of the widest word, so the preview never has to move -->
    <div class="frame" data-frame>
      <!-- the button: a black pill whose letters are movable type. Pressed, the word recomposes itself into the next
           one: the letters the two words share slide to their new place, the others leave and arrive one after the
           other, and the width axis of the typeface keeps every word near the same measure. The letters and the
           tick are laid out by the script on every frame -->
      <button class="bag" type="button" aria-label="Add to bag" data-bag data-autoclic>
        <span class="bag__pill" aria-hidden="true" data-pill>
          <span class="bag__line" data-line>
            <svg class="bag__tick" viewBox="0 0 20 20" data-tick><path d="M4.2 10.6l3.7 3.6 7.9-8.4" pathLength="1" data-tick-path/></svg>
          </span>
        </span>
      </button>
    </div>
    <p class="sr" role="status" aria-live="polite" data-said></p>
    <!-- a hidden copy of the type, to measure every letter at every width and weight -->
    <div class="ruler" aria-hidden="true" data-ruler></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.

23 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 button that recomposes the word” from Animorio and fit it into my site

What it does

On a white page there is only a black pill, 60 pixels high, reading Add to bag in Mona Sans, light on black. When the page opens the pill is born round and the letters come in one after the other, each into its own slot as it opens. On a click the word becomes Adding: the A and the two d stay where they are, «to ba» falls out of the line one letter after the other while its room closes, the g slides left to the end of the new word, and the i and the n open their room and drop into it from above: they touch the line one pixel too low, come back up and take weight for an instant, like a lead sort knocked into the composing stick. Adding is short: its letters widen on the wdth axis to come close to the measure of the word before, and what the axis cannot cover the pill takes, narrowing on a damped spring with a thread of bounce while it gets a hair lower, like rubber. While the shop works, a wave of width and weight runs through Adding from left to right, one letter after another swells and rises by two pixels, and the pill breathes with it. Then Added to bag: the i and the n leave, «ed to ba» arrives, the g runs to the end again, all the letters narrow down to 78 on the width axis to stay in the measure, the room for the tick opens at the front and the tick is drawn in vermilion. After a moment only the e and the d leave, the line closes up, the letters widen again and Add to bag is back. Under the pointer the letters widen one after the other and the pill swells by a few pixels; a press squashes it, wider and lower, and letting go sends it back on the spring.

How it works

The letters in common

To know which letters stay, the script looks for the longest common subsequence of the old word and the new one, with the dynamic programming table, and walks it from the start always taking the leftmost pair. So between Add to bag and Adding the A, d, d and g stay, between Added to bag and Add to bag all of them but the e and the d. Shared letters never cross, because the subsequence keeps their order: between two letters that stay go first the ones that leave, then the ones that arrive, and the line is a single row of letters on the move.

The type measured once

Mona Sans is a variable typeface with a width axis (75 to 125) and a weight axis. At the start a hidden ruler sets every letter of the three words at five widths and five weights and measures its advance with getBoundingClientRect; the advance at any width and weight comes from interpolating between the four nearest values. The letters are separate elements, which lose their kerning: the pairs (Ad, to, ba…) are measured on a canvas in the same face, ab minus a minus b, and put back by hand.

The measure kept by the axis

For each word the script bisects for the axis width that makes it MEASURE pixels long, inside AXIS: Add to bag sits just above 100, Added to bag goes down to 78. Adding is too short to get there, and stops short of the top to leave the wave room to widen. Every letter that stays moves to the new width while it slides, and one that arrives comes in at its own.

The line set on every frame

Every letter is a set of numbers GSAP moves: width, weight, height, opacity and presence, that is how open its slot in the line is. On every frame one function on the GSAP ticker adds up the advances times the presence, centres the line and puts each letter in its place with a translate3d and its font-variation-settings. A letter that leaves closes its slot and one that arrives opens it, pushing its neighbours, with no jump; while it comes or goes it is as wide as its slot (scaleX equal to its presence), so it never covers its neighbours. The g that runs to the end has no animation of its own: it moves because the line recomposes itself around it.

The pill on a spring

The pill’s width is the line’s plus the margin, but it does not follow at once: it is a damped spring (stiffness and damping in SPRING) integrated in half steps, with the time step capped at 1/30 of a second. The height has its own spring too: 60 at rest, a little more under the pointer, a little less under the finger, and while the width races the pill gets a hair lower, 2.5 pixels at most. When springs and tweens are still the ticker removes itself; every timeline wakes it from its onUpdate.

The states and the wave

The click goes to Adding and puts the button in aria-busy; after WAIT seconds comes Added to bag, after HOLD it goes back to Add to bag, and a click during the round does nothing. A hidden text with role=status says Adding to bag and Added to bag. The wave on Adding is a bell running along the word, repeated without end: each letter widens by WAVE[0] and takes weight as the bell passes over it; the change of word fades it out in three tenths of a second.

Values to change

NameFileDefaultWhat it changes
WORDSscript.js['Add to bag', 'Adding', 'Added to bag']The three words: at rest, while the shop works, once it is done (with the tick in front). The more letters they share, the finer the recomposition.
WAITscript.js2.2The seconds of Adding; in a real shop, the time the cart takes to answer.
HOLDscript.js1.8The seconds Added to bag stays before going back to Add to bag.
MEASUREscript.js96The width in pixels the typeface’s axis tries to keep every word at.
AXISscript.js[78, 125]How narrow and how wide the letters may go to keep the measure (Mona Sans goes from 75 to 125).
WEIGHTscript.js[600, 780]The weight of the letters, and the weight a letter takes for an instant when it touches the line.
MOVEscript.js0.75The seconds of one recomposition.
STAGGERscript.js0.045The seconds between one arriving letter and the next (the leaving ones go a little quicker).
DROPscript.js15The pixels a leaving letter falls, and an arriving one falls from.
WAVEscript.js[30, 0.95]The wave on Adding: how much a letter widens as the wave passes over it, and the seconds of one pass.
SPRINGscript.js[520, 0.62]The pill’s spring: its stiffness and its damping (under 1 it goes a little past and comes back).
SWELLscript.js8The pixels the pill swells under the pointer.

Try them in the Playground

When to use it

For the button that puts a product in the cart, and in general for any action with a wait and an outcome (Save, Saving, Saved; Send, Sending, Sent): it works best when the words of the three states share many letters. It needs a variable typeface with a width axis: with a static typeface the letters still recompose, but only the pill keeps the measure.

On phones

The pill is the same, 60 high, and fits easily in 390 pixels even with Added to bag. Without a pointer there is no hover swell: a tap squashes the pill and releases it on the spring, and starts the whole round, with the same animations as on a computer.

With reduced motion

No letters falling or sliding, no wave and no spring: at every change the line fades, the new word is set in its place with the pill already at the right size and the line comes back. During Adding the label pulses gently in opacity, and the tick appears already drawn.

Pitfalls

  • The arriving letters were already whole while their slot was only half open, and covered their neighbours; waiting for the slot to open, instead, the line showed a hole and read Add g. The fix is to tie the width of a letter that comes or goes to its presence (scaleX equal to p): the letter is always as wide as its slot, and becomes visible while it widens.
  • With the new slots opening before the old ones closed, the g of bag went right first and then left. The leaving letters start at once and close their slot in 0.6 times MOVE; the arriving ones start later, so a letter that stays always moves in one direction.
  • Splitting the word into elements loses the kerning and the pairs lose their proper spacing: the computed measure would no longer be the real one. That is why the letters have font-kerning: none and the pairs are measured apart on a canvas.
  • Fontsource’s index.css only loads the weight axis: without wdth.css the font-variation-settings on wdth does nothing and the whole measure is left to the pill.
  • Adding, too short, stopped at 125 and the wave had no room left to widen the letters: for the waiting word the bisection stops at 125 minus WAVE[0].
  • A ticker that turns itself off when nothing moves does not see a timeline’s delayed tweens start: every timeline wakes it from its onUpdate, which GSAP calls even while it waits.

Libraries

GSAP 3.15.0, Mona Sans (font variabile, con gli assi del peso e della larghezza) 5.3.0.

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

Testing

Tested on 16 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