
The water button
Buttons, SVG, Physics, Hover
NewFree
The top bar of a watchmaker: under the links runs a steel rack, and the page you are on is marked by a brass pinion standing on it. Choosing another link the pinion rolls there on a spring, turning exactly as much as it travels, so the teeth always mesh; in its hub a smaller gear turns the other way, and each word lights up as the pinion rolls under it.
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 rack and pinion nav</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 top bar of a watchmaker. Under the links runs a steel rack, and the current page is marked by a brass
pinion standing on it: choosing another link the pinion rolls there, its teeth meshing with the rack's, and a
small gear in its hub turns the other way -->
<header class="bar" data-bar>
<a class="brand" href="#" aria-label="Halvard, home">Halvard</a>
<nav class="strip" aria-label="Main" data-strip>
<div class="track" data-track>
<!-- the rack and the pinion, drawn by the script -->
<svg class="mech" aria-hidden="true" data-mech>
<defs>
<radialGradient id="rp-brass" gradientUnits="userSpaceOnUse" cx="-4" cy="-6" r="22" fx="-5" fy="-7">
<stop offset="0" style="stop-color: var(--brass-hi)"/>
<stop offset="0.32" style="stop-color: var(--brass-light)"/>
<stop offset="0.68" style="stop-color: var(--brass)"/>
<stop offset="1" style="stop-color: var(--brass-dark)"/>
</radialGradient>
<radialGradient id="rp-steel" gradientUnits="userSpaceOnUse" cx="-1.6" cy="-2.2" r="7.5">
<stop offset="0" style="stop-color: var(--steel-hi)"/>
<stop offset="0.45" style="stop-color: var(--steel-light)"/>
<stop offset="1" style="stop-color: var(--steel-dark)"/>
</radialGradient>
<radialGradient id="rp-cavity" gradientUnits="userSpaceOnUse" cx="1" cy="1.5" r="7">
<stop offset="0" style="stop-color: var(--cavity-light)"/>
<stop offset="1" style="stop-color: var(--cavity)"/>
</radialGradient>
<!-- the chamfer round the hole in the hub: in shade where it faces away from the light, lit where it faces it -->
<linearGradient id="rp-chamfer" gradientUnits="userSpaceOnUse" x1="-5" y1="-6" x2="5" y2="6">
<stop offset="0" style="stop-color: var(--brass-deep)"/>
<stop offset="0.5" style="stop-color: var(--brass-dark)"/>
<stop offset="1" style="stop-color: var(--brass-hi)"/>
</linearGradient>
<linearGradient id="rp-rack" x1="0" y1="0" x2="0" y2="1" data-rack-paint>
<stop offset="0" style="stop-color: var(--steel-light)"/>
<stop offset="0.55" style="stop-color: var(--steel)"/>
<stop offset="1" style="stop-color: var(--steel-dark)"/>
</linearGradient>
<filter id="rp-soft" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.3"/>
</filter>
</defs>
<g class="rack" data-rack>
<path class="rack__shadow" filter="url(#rp-soft)" data-rack-shadow/>
<path class="rack__body" fill="url(#rp-rack)" data-rack-body/>
<path class="rack__face" data-rack-face/>
<path class="rack__lit" data-rack-lit/>
<path class="rack__shade" data-rack-shade/>
</g>
<g class="pinion" data-pinion>
<path class="pinion__shadow" filter="url(#rp-soft)" data-shadow/>
<g data-teeth></g>
<path class="pinion__line" data-line/>
<circle class="pinion__core" fill="url(#rp-brass)" data-core/>
<path class="pinion__edge pinion__edge--hi" data-edge="hi"/>
<path class="pinion__edge pinion__edge--lit" data-edge="lit"/>
<path class="pinion__edge pinion__edge--shade" data-edge="shade"/>
<path class="pinion__edge pinion__edge--dark" data-edge="dark"/>
<circle class="pinion__groove" data-groove/>
<circle class="pinion__hole" fill="url(#rp-cavity)" data-hole/>
<circle class="pinion__chamfer" stroke="url(#rp-chamfer)" data-chamfer/>
<path class="pinion__inner-shadow" filter="url(#rp-soft)" data-inner-shadow/>
<g data-inner></g>
<path class="pinion__inner-edge" data-inner-edge/>
<circle class="pinion__arbor" fill="url(#rp-steel)" r="1.55"/>
<circle class="pinion__pivot" r="0.55"/>
</g>
</svg>
<ul class="list" data-list>
<li><a class="link" href="#" data-link data-autoclic><span class="link__word">Watches</span></a></li>
<li><a class="link" href="#" data-link data-autoclic><span class="link__word">Calibres</span></a></li>
<li><a class="link" href="#" aria-current="page" data-link><span class="link__word">Atelier</span></a></li>
<li><a class="link" href="#" data-link data-autoclic><span class="link__word">Service</span></a></li>
<li><a class="link" href="#" data-link data-autoclic><span class="link__word">Journal</span></a></li>
</ul>
</div>
</nav>
<a class="cta" href="#">Book a visit</a>
</header>
</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.
32 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 rack and pinion nav” from Animorio and fit it into my siteThe pinion's outline is worked out once, in polar coordinates: module m = 28 / (TEETH + 1.6), pitch radius m·z/2, a 20 degree pressure angle and stub teeth (0.8 m over the pitch circle, 1 m under it). Between the base circle and the tip each flank is an involute: at radius r half the tooth is ψ(r) = π/2z + inv(20°) − inv(acos(r_b/r)) wide, with inv(a) = tan a − a; under the base circle the flank is radial, and there are two arcs at the tip and in the gap. The rack has straight flanks leaning by the same angle: the profile every involute gear meshes with, and with 14 teeth or more its teeth never dig into the pinion's flanks.
The pinion sits in an SVG group that moves and never turns; every frame its outline is worked out again turned by the angle θ = π/2 + (x − x0) / r, where x0 is the start of the rack: a tooth points down into every gap, and moving by one pitch (π·m) the pinion turns by one tooth. The goals are the middle of each word rounded to the nearest gap, so at rest the teeth always sit in the rack.
Since it is the drawing that turns and not the group, the gradients (userSpaceOnUse) stay where they are: the sheen of the brass is always at the top left. Along the outline every segment is compared with the direction of the light (the dot product of its normal and the light) and goes into one of four paths, bright, lit, shaded and dark, drawn half a pixel inside the edge: it is the polished bevel, and a flank lights up as it turns round to face the light.
The pinion's centre follows a spring with damping worked out from BOUNCE (ζ = −ln B / √(π² + ln² B), so the overshoot is exactly that share of the way), integrated in steps of 1/480 of a second in GSAP's ticker, with the time step capped at 1/30. On top there is a detent: a force −H·k·sin(k(x − x0)) pulling towards the nearest gap, multiplied by exp(−v²/300²), so it is only there at low speed. Near the goal it makes the spring several times stiffer: the last tooth drops into its gap and rocks there. Up to 4.5 times the spring it can never hold the pinion one tooth short of the goal (checked with a simulation from every distance). At the ends of the rack a stop sends it back; at rest the loop leaves the ticker.
Between one frame and the next the teeth turn by an angle that at full speed is more than a whole tooth: up to ten copies of the teeth are drawn back along that angle, half a tooth at most, fainter and fainter, with the one of now on top; the bevel fades with the speed. The gear in the hub turns by −REVERSE·θ with its own streaks. Every link has a --lit variable between 0 and 1, from how close the pinion is to the middle of its word, and the colour is a color-mix of grey and black by that much.
Pointing at a link, or reaching it with Tab (focus-visible), the goal becomes the current link plus NUDGE pitches towards it; the arrow keys move the focus along the links, Home and End go to the ends, Enter and Space choose, and aria-current moves to the chosen link at once. On a phone the links and the rack share one scrolling strip (sizes come from offsetLeft, which ignores the scroll), a finger down on a link gives the push and the tap chooses, with the link sliding to the middle.
| Name | File | Default | What it changes |
|---|---|---|---|
TEETH | script.js | 14 | How many teeth the pinion has (14 or more): it keeps its size, so with more teeth they are finer, on the rack too. |
STIFFNESS | script.js | 190 | How hard the spring pulls towards the link chosen: higher, quicker. |
BOUNCE | script.js | 0.06 | How far it goes past the word before coming back, as a share of the way (0, not at all). |
DETENT | script.js | 4 | How firmly the gaps of the rack hold the teeth as the pinion slows down: the last tooth rocks in its gap before it stops (0 to 4.5). |
NUDGE | script.js | 2 | How many teeth the pinion steps towards a link pointed at, without leaving. |
REVERSE | script.js | 1.7 | How many times faster the small gear in the hub turns the other way. |
BLUR | script.js | true | Teeth spinning fast stretch into streaks and come sharp again as they slow down. |
For the header of people who make mechanical, precise things: a watchmaker, an industrial design studio, makers of bicycles, coffee machines, instruments, furniture with visible hardware, a science and technology museum or an engineering school. It suits a few short links; it is a site bar, always at the top, on a light page.
Below 960 pixels the button goes and the links close up a little; on a phone the bar, still 64 pixels tall, is as wide as the screen less 32 (358 at 390), and below 360 the name too makes room for the links. The links and the rack scroll sideways together inside the bar, which fades out on the side where more are hidden; the pinion stays on the rack and scrolls with it. A finger down on a link gives the two-tooth push, the tap chooses it: the link slides to the middle of the strip while the pinion rolls to it; if the finger scrolls the strip instead, the pinion goes back to its place.
No rolling, no push and no entrance: rack and pinion fade in already in place. Choosing another link the pinion fades out where it is and back in under the new word, already in mesh and turned just right, and the colours of the two words swap with a fade; on a phone the strip jumps to the chosen link without sliding. Presses do not shrink anything.
GSAP 3.15.0, Archivo (font variabile, con l'asse della larghezza) 5.3.0.
Compressed with gzip, the three demo files weigh 12 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.