Skip to content
animorio
IT
All components

The stepper that gains weight

Counters, Text, PhysicsGSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

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 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>

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.

27 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 stepper that gains weight” from Animorio and fit it into my site

What it does

On a white background, at the top left, the word GUESTS, small and grey; under it a minus and a plus inside two very thin rings and, between them, the number of guests, huge, in a sans serif with a variable width. Under the number a thin black line from one end to the other, and twelve ticks hang from the line, one per seat: those up to the number are vermilion, the others light grey. Below, in grey, Tables seat up to 12. The number is the weight: at 1 guest it is hairline thin and narrow, at 12 deep black and wide, and every guest more makes it thicker and a little wider. With plus the old number sinks into the slot just above the line and the new one falls from above, speeding up, one figure after the other; it lands a pixel below its place, squashes a hair and comes back up, and right then it takes its new weight, going a little past and coming back. The line feels the knock where each figure lands, harder the heavier the number: it bends, the wave runs out to both ends, bounces back and dies away, and the hanging ticks ride on it. Meanwhile the new tick lights up and grows with a little spring. With minus the old number floats away upwards and fades and the new, lighter one rises out of the slot, goes a pixel past and settles; the line, let go, gives a small flick upwards. At 12 the plus goes dim: pressed again, the number shakes its head, its weight pushes against the limit and comes back, and the note below turns red, Our longest table seats 12; at 1 the same with minus, At least one guest. With the mouse over a button its ring draws itself from the top; pressed, the sign gets thicker. Holding a button the number keeps changing, faster and faster. A tap on a tick takes the number there, and dragging along the line picks it; from the keyboard the number answers the arrows, Page Up and Down, Home and End.

How it works

The number is a weight

The 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.

The fall and the extra pixel

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.

A line that is a string

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.

The springy no

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.

Keys, touch and the preview

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.

Values to change

NameFileDefaultWhat it changes
MINscript.js1The fewest guests.
MAXscript.js12The most guests, which is also the number of ticks on the line (2 to 24).
STARTscript.js2The guests when the page opens.
LIGHTscript.js[140, 80]The number’s weight (100 to 900) and width (62 to 125) at the fewest guests.
HEAVYscript.js[860, 116]The number’s weight and width at the most guests.
DROPscript.js0.24How long a new number takes to fall into its place, in seconds.
KNOCKscript.js1How hard the line feels a number landing (0 = it does not move).
SPRINGscript.js1How springy the weight is: at 0.5 it settles without wobbling, at 2 it swells past and wobbles.
HOLDscript.js[0.42, 0.16, 0.06]Holding a button: how long before it starts repeating, the first gap and the shortest, in seconds.
WORDscript.js['guest', 'guests']The word for one and for more than one, read out to people using a screen reader.
NOTEscript.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.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The new number left from almost half a figure higher up while the old one was still sinking: for a few hundredths of a second the two overlapped. The old one now sinks in 0.12 seconds and the new one leaves from a third of a figure, 0.09 seconds later: with the sums done on the two curves, the bottom of the new one always stays above the top of the old one.
  • The bottom of the window cuts the figures, but a typeface’s baseline is not the bottom of its line: with bottom: 0 the figures would have hung above the line by the whole descender, which changes from one typeface to another. The script measures where the baseline falls with a zero-height inline block (which rests right on it) and puts it 6 pixels from the bottom of the window, also when the size changes.
  • Had the axes been tweened on the arriving number, the leaving one would have kept the old weight and the two numbers, close together, would have had different thicknesses. The axes sit on the window and one spring drives them, changing its goal when the number lands.
  • A mouse click comes after pointerdown, which has already counted: it counted twice. The click that follows a press is ignored, but Enter and Space on a button only make the click (with detail 0, like the preview’s): the preview is told apart because its click is not isTrusted.
  • With the drawn ring in place at rest, stroke-dasharray left a speck at the top of the buttons even at dashoffset 1: at rest the drawn ring also has zero opacity.
  • The focus box around the whole number cut through the word GUESTS, which sits in the empty top of the window: the box starts a third of the way down the window and only hugs the figures.

Libraries

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.

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