Skip to content
animorio
IT
All components

The wire stepper

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

How many tickets for a concert: the number is not typed, it is a white wire bent into a digit. With plus and minus the wire bends into the next digit curve by curve, never crumpling; pushed past the limit it stretches towards the button like a rubber band, warms to yellow and, let go, snaps back wobbling.

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 wire stepper</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/schibsted-grotesk@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the tickets of a concert: a label and a stepper. The number is one white wire bent into a digit: changing
         it, the wire bends into the next digit on springs; pushed past the limit it stretches towards the button
         like a rubber band and snaps back. The wire is drawn by the script -->
    <div class="tickets" data-tickets>
      <div class="tickets__label">
        <span class="tickets__name" id="tickets-name">General admission</span>
        <span class="tickets__note" id="tickets-note"><span class="tickets__note-in" data-note>€38 each</span></span>
      </div>

      <div class="stepper" data-stepper>
        <button class="stepper__btn" type="button" aria-label="Remove one ticket" data-less>
          <span class="stepper__disc" aria-hidden="true">
            <svg viewBox="0 0 24 24"><path d="M6.5 12h11"/></svg>
          </span>
        </button>
        <div class="stepper__value" role="spinbutton" tabindex="0" aria-labelledby="tickets-name" aria-describedby="tickets-note" aria-valuemin="1" aria-valuemax="4" aria-valuenow="3" aria-valuetext="3 tickets" data-value>
          <svg class="stepper__wire" viewBox="-100 -70 200 140" aria-hidden="true" data-wire></svg>
        </div>
        <button class="stepper__btn" type="button" aria-label="Add one ticket" data-more data-autoclic>
          <span class="stepper__disc" aria-hidden="true">
            <svg viewBox="0 0 24 24"><path d="M6.5 12h11M12 6.5v11"/></svg>
          </span>
        </button>
      </div>

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

28 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 wire stepper” from Animorio and fit it into my site

What it does

On a white background a row: on the left General admission and below it, in grey, €38 each; on the right a cobalt blue pill 60 pixels tall with a minus and a plus on two lighter discs and, between them, the number 3, which is not typed in a font but bent out of a white wire. When the page opens the wire arrives straight, like a third minus, and bends into the 3. With plus the wire bends into the next digit curve by curve: the bend runs from one end of the wire to the other, goes a little too far and comes back, and the wire gives a small hop upwards; with minus it bends the same way and dips. At 4 the plus goes dim and Max 4 per order rises under the label, in blue. Pressing plus again the number does not change: the wire stretches towards the button like a rubber band, thinner and yellow, the note leans towards the button and, let go, the wire snaps back wobbling; held down it stretches further and further. The same with minus below 1, with At least 1 ticket. A button held down repeats, faster and faster; from the keyboard the number answers the arrows, Home and End. With MAX above 9, at ten the wire splits into two digits and going back to nine the two merge again.

How it works

One digit, one stroke

Every digit is a single stroke, written with M, L and C in a box 56 wide and 100 tall (the 0 and the 8 are closed loops). The script splits it into 96 points evenly spaced along the stroke and centres it. On screen the wire is a path through the midpoints with the points as handles (Q), so it never has corners, with its shadow just below it.

Bending, not moving

The wire as it is and the next digit are read as the direction of each of the 95 little segments, unrolled so it never jumps by a whole turn, and its length. During the change every segment turns from its old direction to its new one and the wire is rebuilt by adding the segments up; then it is moved to where its middle should be. So half way through it is still a wire, curling and uncurling, instead of the tangle that points moved in straight lines make.

The way that bends least

The next digit can be laid along the wire forwards or backwards, and a loop starting from any of its points: the script tries every way (190 for the 0 and the 8), takes off each one the whole turns that separate it from the wire as it is, and picks the one that asks for the least bending and the shortest trip.

The ripple and the springs

Each segment starts turning a little after the one before: the bend runs along the wire in RIPPLE seconds, from the first segment to the last when adding and the other way when removing, and each one turns in BEND seconds with back.out(OVERBEND), which takes it a little too far. The real wire follows this shape on one spring per point, also held by its neighbours (WIRE): the springs make the hop (THROW) and smooth over a change that comes before the last one has finished.

The limit as a rubber band

Past the minimum or the maximum the number stays, but the shape to follow is pulled towards the button: the far side stays put, the points move more the nearer they are to the button (to a power of 1.6) and the digit gets a little squashed in height. The strain goes to 0.55 on the first push and towards 1 on every repeat; the wire loses up to a quarter of its thickness and takes the colour of --strain. Let go, the strain goes back to zero in 0.06 seconds and the springs do the bounce.

Fingers, keys and the preview

On pointerdown the button counts at once and after HOLD[0] seconds it repeats, each gap a fifth shorter than the one before, down to HOLD[2]; while holding, the bend lasts a little less than the gap, so every digit has time to form. The click that follows a press does not count a second time, those from the keyboard and the preview do. The number is a role="spinbutton" with aria-valuetext; from the buttons an aria-live region says the number once the pressing is over.

Values to change

NameFileDefaultWhat it changes
MINscript.js1The smallest number that can be chosen.
MAXscript.js4The largest number that can be chosen (up to 99, with two digits).
STARTscript.js3The number when the page opens.
BENDscript.js0.46How many seconds each bit of the wire takes to bend into the next digit.
OVERBENDscript.js1.5How far the bends go past the next digit before coming back (0 = not at all).
RIPPLEscript.js0.14How many seconds the bend takes to run along the wire from one end to the other.
THROWscript.js150How far the wire hops: up when adding, down when removing.
SPRINGscript.js[600, 0.42]The wire’s springs: how stiff they are and how much they are held back (1 = no wobble).
WIREscript.js8How much the wire holds together on its springs: 0 every point on its own, more a stiffer wire.
STRETCHscript.js60How far the wire stretches when pushed past the limit, in digit units (a digit is 100 tall).
HOLDscript.js[0.42, 0.16, 0.05]Holding a button: how long before it starts repeating, the first gap and the shortest, in seconds.
WORDscript.js['ticket', 'tickets']The word read after the number, for one and for more than one.

Try them in the Playground

When to use it

For choosing how many: tickets, guests at dinner, nights in a hotel, items in a cart, portions, cups in an order. It suits places with a real limit to make felt, like a concert’s tickets per person, and where the number matters: it is as small as any control, but every change shows.

On phones

On a phone the row fits in 390 pixels, the label on the left and the 60 pixel stepper on the right. A finger works like the mouse: a tap counts one, held down it repeats faster and faster and past the limit it pulls the wire; a long press opens no menu and selects nothing, and two quick taps do not zoom the page.

With reduced motion

The wire does not bend or hop: the new digit is in place at once and fades in. Past the limit the wire does not stretch, it only turns yellow for a moment, and the note under the label changes with a fade instead of sliding.

Pitfalls

  • Moving each point in a straight line to its place in the next digit sent the wire through the middle: half way it was a crumpled, smaller tangle, even with the springs between neighbours. Interpolating the direction of the segments instead of their position keeps the wire a wire, and it bends.
  • The directions have to be unrolled: atan2 jumps from 180 to −180 degrees and that segment would turn a whole way round backwards. The difference, brought between −180 and 180 degrees, is added up, and the next digit loses the whole turns that separate it from the wire as it is.
  • A wire rebuilt from its directions alone has no place of its own and, half way through a bend, drifts off: on every frame it is put back with its middle where it should be, between the old middle and the new one.
  • Playwright, and with it the test run, will not click a button with aria-disabled: the data-autoclic mark moves to the other button as soon as a limit is pushed, and at the start if it begins at the maximum.
  • The click that comes after a press has to be ignored, because pointerdown has already taken the step; but if the finger slides off the button the click never comes, and the mark left on would have eaten the keyboard’s next Enter. It is cleared on the frame after the release.
  • When the note under the label changed, the row changed width and the stepper, centred with it, slid sideways. The three notes sit one over the other in the same grid cell: the label is always as wide as the longest.
  • Half way through a bend some wires (the 0 opening into the 1, the 3 turning into a 4) are taller than a digit and went out of the pill, cut by its edge: if the shape is taller than ROOM it shrinks a little round its middle, and if it still pokes out it is moved back in.
  • With the springs tied to their neighbours the sums get stiff and blow up on a slow frame: the number of steps is worked out from the real stiffness, half a radian at most per step.

Libraries

GSAP 3.15.0, Schibsted Grotesk (font variabile) 5.3.0.

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

Testing

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