
The copy button
Buttons, Text, SVG, Physics
NewFree
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.
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>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
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 siteTo 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.
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.
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.
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’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 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.
| Name | File | Default | What it changes |
|---|---|---|---|
WORDS | script.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. |
WAIT | script.js | 2.2 | The seconds of Adding; in a real shop, the time the cart takes to answer. |
HOLD | script.js | 1.8 | The seconds Added to bag stays before going back to Add to bag. |
MEASURE | script.js | 96 | The width in pixels the typeface’s axis tries to keep every word at. |
AXIS | script.js | [78, 125] | How narrow and how wide the letters may go to keep the measure (Mona Sans goes from 75 to 125). |
WEIGHT | script.js | [600, 780] | The weight of the letters, and the weight a letter takes for an instant when it touches the line. |
MOVE | script.js | 0.75 | The seconds of one recomposition. |
STAGGER | script.js | 0.045 | The seconds between one arriving letter and the next (the leaving ones go a little quicker). |
DROP | script.js | 15 | The pixels a leaving letter falls, and an arriving one falls from. |
WAVE | script.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. |
SPRING | script.js | [520, 0.62] | The pill’s spring: its stiffness and its damping (under 1 it goes a little past and comes back). |
SWELL | script.js | 8 | The pixels the pill swells under the pointer. |
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.
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.
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.
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.
Tested on 16 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.