
The needle gauge
Counters, Physics, SVG
NewFree
Pairing a smart bulb in a home app, and the progress bar is the bulb itself, seen from the side: as the pairing goes on its tungsten coil warms through the real colours of a hot body, from dull red to orange to a warm white, the glass fills with light and the coil trembles a little on its wires. If the link drops the coil flickers and cools; once paired the bulb comes on in full, with a flash, and its light spreads over the panel.
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, viewport-fit=cover">
<title>The pairing bulb</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">
<!-- the pairing bulb: a smart home app pairing a new filament bulb, and the bulb is the progress. Seen from the side,
its tungsten coil warms through the real colours of a hot body as the pairing goes on, from a dull red to
orange to a warm white, while the glass fills with light and the coil trembles on its wires. If the link
drops, the coil flickers and cools; once paired, the bulb comes on in full and its light spreads over the
panel. The coil, the halo and the light are drawn by script.js -->
<section class="pair" aria-labelledby="pair-title" data-pair>
<div class="pair__light" aria-hidden="true" data-light></div>
<header class="pair__head">
<div class="pair__words">
<h2 class="pair__title" id="pair-title"><span class="pair__swap" data-title>Living room lamp</span></h2>
<p class="pair__meta"><span class="pair__swap" data-meta>New bulb nearby · Not paired</span></p>
</div>
<p class="pair__kelvin" aria-hidden="true"><span data-kelvin>Off</span></p>
</header>
<div class="pair__bulb" role="progressbar" aria-labelledby="pair-title" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="0" aria-valuetext="Not paired" data-bar>
<svg class="pair__art" viewBox="10 30 180 232" aria-hidden="true" data-art>
<defs>
<clipPath id="pb-glass-clip"><path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188Z"/></clipPath>
<radialGradient id="pb-halo-wide" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="84">
<stop offset="0" stop-color="#ffb070" stop-opacity="0" data-stop="wide0"/>
<stop offset="0.38" stop-color="#ffb070" stop-opacity="0" data-stop="wide1"/>
<stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="wide2"/>
</radialGradient>
<radialGradient id="pb-halo-core" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="26">
<stop offset="0" stop-color="#fff4e6" stop-opacity="0" data-stop="core0"/>
<stop offset="1" stop-color="#fff4e6" stop-opacity="0" data-stop="core1"/>
</radialGradient>
<radialGradient id="pb-halo-mid" gradientUnits="userSpaceOnUse" cx="100" cy="96" r="36">
<stop offset="0" stop-color="#ffb070" stop-opacity="0" data-stop="mid0"/>
<stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="mid1"/>
</radialGradient>
<linearGradient id="pb-glass-edge" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.34"/>
<stop offset="0.5" stop-color="#ffffff" stop-opacity="0.08"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.22"/>
</linearGradient>
<linearGradient id="pb-glass-fill" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.07"/>
<stop offset="0.55" stop-color="#ffffff" stop-opacity="0.015"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.05"/>
</linearGradient>
<linearGradient id="pb-metal" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color: var(--metal-lo)"/>
<stop offset="0.2" style="stop-color: var(--metal-mid)"/>
<stop offset="0.36" style="stop-color: var(--metal-hi)"/>
<stop offset="0.62" style="stop-color: var(--metal-mid)"/>
<stop offset="1" style="stop-color: var(--metal-lo)"/>
</linearGradient>
<linearGradient id="pb-stem" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.2"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0.05"/>
</linearGradient>
<linearGradient id="pb-base-lit" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffb070" stop-opacity="0.55" data-stop="base0"/>
<stop offset="1" stop-color="#ffb070" stop-opacity="0" data-stop="base1"/>
</linearGradient>
<filter id="pb-bloom" x="-60%" y="-160%" width="220%" height="420%" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="1.4" result="near" data-blur="near"/>
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="far" data-blur="far"/>
<feMerge><feMergeNode in="far"/><feMergeNode in="far"/><feMergeNode in="near"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- the screw base: a collar, four threads, the black insulator and the contact -->
<g class="pair__base">
<path d="M74 186h52v8c0 3-2 5-5 5H79c-3 0-5-2-5-5z" fill="url(#pb-metal)"/>
<rect x="78" y="198" width="44" height="40" style="fill: var(--metal-lo)"/>
<g data-threads>
<rect x="73" y="199" width="54" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 203.5)"/>
<rect x="74" y="209" width="52" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 213.5)"/>
<rect x="75" y="219" width="50" height="9" rx="4.5" fill="url(#pb-metal)" transform="rotate(-3 100 223.5)"/>
<rect x="77" y="229" width="46" height="8" rx="4" fill="url(#pb-metal)" transform="rotate(-3 100 233)"/>
</g>
<path d="M80 238h40l-4 9H84z" style="fill: var(--insulator)"/>
<path d="M90 247h20c0 5-4 8-10 8s-10-3-10-8z" fill="url(#pb-metal)"/>
<path d="M74 186h52v14H74z" fill="url(#pb-base-lit)" style="mix-blend-mode: screen" data-base-lit/>
</g>
<!-- the glass and everything inside it -->
<path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188Z" fill="url(#pb-glass-fill)"/>
<g clip-path="url(#pb-glass-clip)">
<rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-wide)"/>
<rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-mid)"/>
<rect x="0" y="0" width="200" height="200" fill="url(#pb-halo-core)"/>
</g>
<!-- the stem: a glass tube flaring into the neck, with the press that holds the wires -->
<path d="M84 192C88 184 90 168 91 150Q100 145 109 150C110 168 112 184 116 192Z" fill="url(#pb-stem)"/>
<path d="M98 150V120" style="stroke: var(--stem)" stroke-width="2.6" stroke-linecap="round" opacity="0.55"/>
<g class="pair__wires" fill="none" stroke-linecap="round" stroke-linejoin="round" data-wires>
<path d="M94 156L94 140L74 100L74 93" data-wire="0"/>
<path d="M106 156L106 140L126 100L126 93" data-wire="1"/>
<path d="M98 121L100 104" data-wire="2"/>
</g>
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path class="pair__coil pair__coil--back" data-coil="back"/>
<g filter="url(#pb-bloom)" data-glow>
<path class="pair__coil pair__coil--glow" data-coil="glow"/>
</g>
<path class="pair__coil pair__coil--front" data-coil="front"/>
<path class="pair__coil pair__coil--core" data-coil="core"/>
</g>
<!-- the glass's own edge and its two highlights, over everything -->
<path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188" fill="none" stroke="url(#pb-glass-edge)" stroke-width="1.2"/>
<path d="M75 188C75 166 38 144 38 98A62 62 0 0 1 162 98C162 144 125 166 125 188" fill="none" stroke="#ffb070" stroke-width="1.3" opacity="0" data-rim/>
<path d="M50 78C56 60 70 48 86 43" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round" opacity="0.26"/>
<path d="M47 96C47 92 47.5 89 48.5 86" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round" opacity="0.2"/>
<path d="M150 122C146 132 140 140 133 147" fill="none" stroke="#ffffff" stroke-width="1.6" stroke-linecap="round" opacity="0.12"/>
</svg>
</div>
<footer class="pair__foot">
<div class="pair__readout" aria-hidden="true">
<p class="pair__pct" data-pct-row>
<svg class="pair__tick" viewBox="0 0 24 24" data-tick><path d="M5 12.8l4.2 4.2L19 7.2" pathLength="1"/></svg>
<span class="pair__swap" data-pct>Smart bulb</span>
</p>
<p class="pair__sub"><span class="pair__swap" data-sub>E27 · 806 lm · dimmable</span></p>
</div>
<button class="pair__btn" type="button" data-action data-autoclic><span class="pair__label" data-label>Pair</span></button>
</footer>
<p class="sr" role="status" aria-live="polite" data-live></p>
</section>
</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.
31 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 pairing bulb” from Animorio and fit it into my siteThe coil’s colour is not a gradient picked by hand. For every temperature the script sums Planck’s law, B(λ, T) = 1 ÷ (λ⁵ (e^(c₂ ÷ λT) − 1)), over the whole visible spectrum in steps of 5 nanometres, weighed by the three curves of the CIE 1931 observer (the analytic fit by Wyman, Sloan and Shirley: a few skewed gaussians per curve). Out come X, Y and Z, which a matrix turns into linear sRGB; the strongest channel is brought to 1 and the gamma curve applied. So 900 K gives a deep red, 1500 an orange, 2700 the warm white of a bulb, without any of them written down anywhere. It is worked out once at the start, a table every 10 kelvin.
The luminance Y of a hot body grows hugely with its temperature: at 1000 K it is a millionth of what it is at 2700. The script takes its logarithm between the first visible red (about 700 K) and the bulb’s colour, the way an eye adapts: that intensity decides how lit the coil is, how far its core goes towards white, the strength of the bloom, of the three halos in the glass and of the light on the panel. The real temperature chases the one wanted with a first order lag, quicker to heat than to cool, so a coil switched off fades from yellow to red to grey.
The coil is a helix along a sagging curve between the two wires: for every point θ = 2πNt, the height is the radius times sin θ and the depth cos θ, which, seen at a slant, moves the point sideways. Points with a positive depth go into the front path, the others into the back one (darker and thinner), and every run starts at the last point of the other, so the turns close without gaps. Over it lies a copy of the whole coil with an SVG bloom filter (two feGaussianBlurs merged with feMerge, widening with the intensity) and a thin core that turns white when the bulb is full. As it heats the tungsten grows longer and the coil sags a little more; the support wire in the middle follows it.
The coil trembles like a string held at both ends: two modes up and down (half a wave and a whole one, at 6 and 11.5 swings a second) and one sideways, each a damped spring worked out in steps of 1/480 of a second, driven by noise that grows with the work. Every piece of firmware that arrives gives it a tap and a little more heat; when the link drops, every flicker of the current shakes it.
Inside the glass, clipped to its shape, three radial gradients are coloured every frame: a wide one that fills the bulb, a middle one round the coil and an almost white core that lights up only near full power; the edge of the glass and the top of the base take on the colour of the light. Outside, behind the bulb, two radial gradients on the panel: a tight one round the glass and a wide one that, once the bulb is on, spreads over the whole panel on an expo.out curve while a short flash takes the coil to about 3200 K and lets it settle back to 2700.
The bulb is a role=progressbar with aria-valuenow and an aria-valuetext that names the phase, the panel has aria-busy while it works and an aria-live line says when the bulb is found, connects, loses the link and is paired. The button changes its word (Pair, Cancel, Retry, Pair again) and the pill takes the new width with a little bounce; a click from a script, the library preview’s, never cancels.
| Name | File | Default | What it changes |
|---|---|---|---|
SECONDS | script.js | 7 | How long a pairing takes in the demo, in seconds. |
KELVIN | script.js | 2700 | The colour temperature of the bulb once it is on, in kelvin (2700 is the warm white of a filament bulb). |
WARMEST | script.js | 2250 | How hot the coil gets just before the pairing ends, in kelvin. |
TURNS | script.js | 15 | How many turns the tungsten coil has. |
TREMBLE | script.js | 1 | How much the coil trembles on its wires while it works: 0 not at all. |
FLASH | script.js | 1 | The flash when the bulb comes on in full: 0 none, 1 a clear one. |
FAIL_EVERY | script.js | 2 | In the demo, one pairing in how many loses the link (0 never). |
FAIL_AT | script.js | 0.66 | How far the pairing that fails gets, from 0 to 1 (always during the firmware update). |
FIRMWARE | script.js | '4.2.1' | The firmware version the bulb receives. |
NETWORK | script.js | 'Home Wi-Fi' | The network the bulb joins. |
For pairing or setting up a home device (a bulb, a plug, a thermostat), updating a device’s firmware, or any wait of a few seconds in a product with a warm tone, where the wait can be told as «we are switching it on». The warming colour says how far along things are even without reading the percentage.
The panel takes the screen’s width less 32 pixels (358 on a 390 screen) and stays 349 tall; the bulb is vector and stays sharp at every density, and the light on the panel centres itself on the bulb again when the window changes size. The button is a pill 38 tall: under a finger it squashes and springs back, and its words widen on the width axis only under the mouse.
The coil neither trembles nor flickers, and there is no flash: when the link drops the current fades out and the coil cools; once the bulb is on the light on the panel fades in instead of spreading. The coil still warms with the progress, because its colour is the indicator; the new words fade in.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 14 KB; the libraries: GSAP 28 KB.
Tested on 8 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.