Skip to content
animorio
IT
All components

The rack and pinion nav

Navigation, Hover, Physics, SVGGSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

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.

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

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.

32 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 rack and pinion nav” from Animorio and fit it into my site

What it does

On a white background a white bar 64 pixels tall with a soft shadow: on the left the name Halvard in wide, spaced capitals, in the middle five small uppercase links (Watches, Calibres, Atelier, Service, Journal), on the right the Book a visit button with a thin border. Under the links runs a steel rack, a bar with trapezoidal teeth, and under the current link, dark, stands a 28 pixel brass pinion with its teeth in the rack's: a polished toothed ring, a dark window in the hub and inside it a smaller steel gear. The light comes from the top left and stays there: the flanks of the teeth facing it shine, the others are in shade, and the pinion casts a shadow on the bar. On arrival the rack rises into the bar and the pinion rolls in from the left end to the current link. Choosing another link the pinion rolls over to it: it sets off with a rush, spins so fast that its teeth stretch into streaks, goes a little past the word, comes back and slows down, the teeth come sharp again and the last one drops into its gap in the rack, rocks there a moment and stops. While it rolls, the gear in the hub turns the other way and faster, and every word the pinion passes under darkens and turns grey again, up to the chosen one, which stays dark. With the mouse on another link, or reaching it with Tab or the arrow keys, the pinion steps two teeth towards it and waits there, straining, without leaving; taking the mouse away it goes back to its place. Clicking the link you are already on, the pinion rocks in its gap. On a phone the links scroll sideways inside the bar with the rack, the chosen link comes to the middle and the pinion rolls to it.

How it works

Involute teeth made by the code

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

Rolling without slipping

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.

The light that stays put

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 spring and the detent

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.

Streaking teeth and words that light up

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.

Hand, keys and phone

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.

Values to change

NameFileDefaultWhat it changes
TEETHscript.js14How many teeth the pinion has (14 or more): it keeps its size, so with more teeth they are finer, on the rack too.
STIFFNESSscript.js190How hard the spring pulls towards the link chosen: higher, quicker.
BOUNCEscript.js0.06How far it goes past the word before coming back, as a share of the way (0, not at all).
DETENTscript.js4How 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).
NUDGEscript.js2How many teeth the pinion steps towards a link pointed at, without leaving.
REVERSEscript.js1.7How many times faster the small gear in the hub turns the other way.
BLURscript.jstrueTeeth spinning fast stretch into streaks and come sharp again as they slow down.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • With full-depth teeth and a 25 degree pressure angle the pinion's teeth came to a point and at rest it looked like a little sun: stub teeth at 20 degrees (0.8 and 1 module) have flat tops and read as a gear, and from 14 teeth the rack never undercuts them.
  • Turning the pinion with a transform, the gradients and highlights turned with it, as if the lamp were glued to the brass: the group only moves, and the outline is worked out again, turned, at every frame.
  • The first blur (equal see-through copies along the whole angle of the frame) turned the moving pinion into a soft golden disc, a glowing dot: now the copies cover half a tooth at most, fade towards the back and the one of now is on top, so you see teeth leaving streaks.
  • With the steel gradient starting from white, the rack's teeth vanished into the white bar and only their shaded flanks were left, like a row of thorns: now the steel is darker, with a fine outline and light only on the flanks that face it.
  • A detent strong enough to be seen everywhere held the pinion one tooth short of the goal: now it only works at low speed (it fades with the speed) and stops at 4.5 times the spring, the most for which a simulation from every distance always reaches the right gap.
  • On a phone the page's grid column grew as wide as all the links and the bar went off the screen: the column is minmax(0, 1fr), so the strip scrolls instead.
  • The focus outline round the word touched the pinion's teeth below it: now it is an ::after drawn close round the capitals, leaving two pixels of air.

Libraries

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.

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