
The needle gauge
Counters, Physics, SVG
NewFree
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.
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>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
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 siteEvery 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.
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 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.
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.
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.
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.
| Name | File | Default | What it changes |
|---|---|---|---|
MIN | script.js | 1 | The smallest number that can be chosen. |
MAX | script.js | 4 | The largest number that can be chosen (up to 99, with two digits). |
START | script.js | 3 | The number when the page opens. |
BEND | script.js | 0.46 | How many seconds each bit of the wire takes to bend into the next digit. |
OVERBEND | script.js | 1.5 | How far the bends go past the next digit before coming back (0 = not at all). |
RIPPLE | script.js | 0.14 | How many seconds the bend takes to run along the wire from one end to the other. |
THROW | script.js | 150 | How far the wire hops: up when adding, down when removing. |
SPRING | script.js | [600, 0.42] | The wire’s springs: how stiff they are and how much they are held back (1 = no wobble). |
WIRE | script.js | 8 | How much the wire holds together on its springs: 0 every point on its own, more a stiffer wire. |
STRETCH | script.js | 60 | How far the wire stretches when pushed past the limit, in digit units (a digit is 100 tall). |
HOLD | script.js | [0.42, 0.16, 0.05] | Holding a button: how long before it starts repeating, the first gap and the shortest, in seconds. |
WORD | script.js | ['ticket', 'tickets'] | The word read after the number, for one and for more than one. |
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 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.
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.
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.
Tested on 6 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.