
The copy button
Buttons, Text, SVG, Physics
NewFree
In the minimal style: how many guests for a table, from 1 to 12, with a minus, a plus and one big number that gets heavier and a little wider with every guest. The new number falls into its place, lands a pixel too low and comes back up, and the thin line under it feels the knock: it shakes like a string and settles. Twelve ticks hang from the line, red up to the number.
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 stepper that gains weight</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- how many guests for a table: minus, the number, plus. Every guest more makes the number heavier and a little
wider; the new number falls into its place, lands a pixel too low and the thin line under it feels the knock.
Under the line, one thin tick per seat. The numbers, the line and the ticks are made by the script -->
<div class="party" data-party>
<p class="party__label" id="party-label">Guests</p>
<div class="party__row">
<button class="party__btn" type="button" aria-label="Remove a guest" data-less>
<span class="party__btn-in" aria-hidden="true">
<svg viewBox="0 0 48 48">
<circle class="party__ring" cx="24" cy="24" r="23"/>
<circle class="party__draw" cx="24" cy="24" r="23" pathLength="1"/>
<path class="party__icon" d="M17 24h14"/>
</svg>
</span>
</button>
<div class="party__value" role="spinbutton" tabindex="0" aria-labelledby="party-label" aria-describedby="party-note" aria-valuemin="1" aria-valuemax="12" aria-valuenow="2" aria-valuetext="2 guests" data-value>
<span class="party__window" aria-hidden="true" data-window></span>
</div>
<button class="party__btn" type="button" aria-label="Add a guest" data-more data-autoclic>
<span class="party__btn-in" aria-hidden="true">
<svg viewBox="0 0 48 48">
<circle class="party__ring" cx="24" cy="24" r="23"/>
<circle class="party__draw" cx="24" cy="24" r="23" pathLength="1"/>
<path class="party__icon" d="M17 24h14M24 17v14"/>
</svg>
</span>
</button>
</div>
<svg class="party__ruler" aria-hidden="true" data-ruler>
<path class="party__rule" data-rule d=""/>
<g data-ticks></g>
</svg>
<p class="party__note" id="party-note" data-note></p>
<p class="sr" aria-live="polite" data-said></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>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.
27 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 stepper that gains weight” from Animorio and fit it into my siteThe figures are set in Archivo, a variable typeface with two axes, weight (wght) and width (wdth). The count does not change the axes directly: it drives a damped spring (OMEGA 17, damping 0.62 / SPRING) heading for the new count, and weight and width are read from where the spring is, between LIGHT and HEAVY. So the weight grows in a moment, goes a little past and settles, and jumping from 3 to 12 with End the number swells all in one go. The axes sit on the whole window, so the number arriving and the one leaving always have the same weight.
Each number is a row of figures (inline-block) in a window that fades out at the top and cuts sharply at the bottom, just above the line: that is the slot. With plus the old number sinks into the slot in 0.12 seconds (power2.in) and the new one leaves 0.09 seconds later from a third of a figure higher up, so the two never touch; it falls with power2.in, like something let go, to 1.2 pixels below its place and comes back up with elastic.out while the whole number squashes 2.5% on its bottom. Two figures fall one after the other, 35 thousandths apart.
The line is a string of 56 pieces with fixed ends: at every step (a thousandth of a second at most, and a slow frame counts as 1/30) each point gets an acceleration from the curvature between its two neighbours, with a wave that runs at 2600 pixels a second, and loses speed to friction. A figure that lands gives the string a bell-shaped push right under its centre, measured with getBoundingClientRect, and the push grows with the weight of the number: at 3 the line bends by a pixel and a half, jumping from 3 to 12 by more than four. The ticks hang from the string and are drawn wherever it goes. When string and spring are still the loop takes itself off GSAP’s ticker and draws nothing.
At a limit the number does not change: it moves 6 pixels towards the button and comes back with a tight elastic.out, which makes two small noes; the button’s sign does the same by 3 pixels. The weight’s spring gets a push past the limit and comes back, the string a small knock, and the note under the ticks rolls up its red sentence and goes back to the usual one after 1.8 seconds.
The number is a spinbutton with aria-valuenow and aria-valuetext, and an aria-live region reads the count half a second after the last change. A button counts on the press (pointerdown) and repeats while held, with gaps shrinking by 80% down to HOLD[2]; the click that follows is ignored, while Enter and Space count in the click. The line takes taps and drags (touch-action: pan-y, so the page still scrolls). A click that comes from a script, the one of the library’s preview, plays a whole tour in a GSAP timeline instead: up to 12, one plus too many, and down to the number it started from.
| Name | File | Default | What it changes |
|---|---|---|---|
MIN | script.js | 1 | The fewest guests. |
MAX | script.js | 12 | The most guests, which is also the number of ticks on the line (2 to 24). |
START | script.js | 2 | The guests when the page opens. |
LIGHT | script.js | [140, 80] | The number’s weight (100 to 900) and width (62 to 125) at the fewest guests. |
HEAVY | script.js | [860, 116] | The number’s weight and width at the most guests. |
DROP | script.js | 0.24 | How long a new number takes to fall into its place, in seconds. |
KNOCK | script.js | 1 | How hard the line feels a number landing (0 = it does not move). |
SPRING | script.js | 1 | How springy the weight is: at 0.5 it settles without wobbling, at 2 it swells past and wobbles. |
HOLD | script.js | [0.42, 0.16, 0.06] | Holding a button: how long before it starts repeating, the first gap and the shortest, in seconds. |
WORD | script.js | ['guest', 'guests'] | The word for one and for more than one, read out to people using a screen reader. |
NOTE | script.js | ['Tables seat up to {max}', 'Our longest table seats {max}', 'At least one guest'] | The note under the ticks: at rest, at the most and at the fewest; {max} becomes the largest number. |
For choosing how many people or things when the number is small and it matters: the guests of a restaurant booking, the seats at a table, the people on a guided tour, nights, tickets, portions. It suits a quiet, typographic site that wants a gesture you can feel without drawings around it: the number itself tells how big the party is.
The number shrinks with the width of the screen (30% of it at most), so at 390 pixels a deep black, wide 12 still fits between the two buttons and nothing overflows. A finger works like the mouse: a tap on a button counts one and draws the ring, held down it repeats faster and faster, a tap on a tick takes the number there and dragging along the line picks it, while the page still scrolls up and down. A long press opens no menu and two quick taps do not zoom the page.
No falls, bounces, shakes or shaking strings: the new number fades in at its place while the old one fades out, the weight changes without going past, the ticks light up at once and the note changes with a fade. Keys, buttons and the line work as always.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 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.