
The time wheel
Pickers, 3D, Physics
NewFree
The bottom bar of a voice recorder app, made like the row of keys of an old tape deck: five long cream keys with an aluminium front, and the one of the chosen tab pressed and latched down. Pressing another, through the slot under the keys you see the steel latch bar slide aside: it lets the old key go, which springs back up with a bounce, and catches in the notch of the new one.
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 interlocking keys tab bar</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 tab bar of a voice recorder app, built like the row of keys of an old tape deck: five long piano keys of
cream plastic with an aluminium front, on a brushed metal plate. The tab you are on is held down by a steel
latch bar, seen through the slot under the keys. Pressing another key, its lug pushes the bar aside, the bar
lets the old key go (it springs back up), then catches the new one in its notch. The keys, the bar and its
spring are worked out by the script on every frame -->
<nav class="deck" aria-label="Main" data-deck>
<span class="deck__well" aria-hidden="true"></span>
<span class="deck__slot" aria-hidden="true">
<svg class="deck__works" data-works></svg>
</span>
<div class="deck__keys">
<button class="key" type="button" data-key data-autoclic>
<span class="key__body" data-body>
<span class="key__top" data-top>
<svg class="key__icon" viewBox="0 0 24 24" aria-hidden="true">
<rect class="key__fill" x="8.6" y="3.2" width="6.8" height="11.2" rx="3.4"/>
<rect x="8.6" y="3.2" width="6.8" height="11.2" rx="3.4"/>
<path d="M5.6 11.2a6.4 6.4 0 0 0 12.8 0M12 17.6v3.2M8.8 20.8h6.4M10.8 7h2.4M10.8 9.6h2.4"/>
</svg>
<span class="key__shade" data-shade></span>
</span>
<span class="key__front" data-front>
<span class="key__name">Record</span>
<span class="key__gloss" data-gloss></span>
</span>
</span>
</button>
<button class="key" type="button" data-key>
<span class="key__body" data-body>
<span class="key__top" data-top>
<svg class="key__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="key__fill" d="M3.6 6.4h16.8v11.2H3.6z"/>
<rect x="3.2" y="5.6" width="17.6" height="12.8" rx="1.8"/>
<path d="M6.2 18.4l1.6-3.4h8.4l1.6 3.4M7.4 10.4h9.2"/>
<rect x="6.6" y="8.6" width="3.6" height="3.6" rx="1.8"/>
<rect x="13.8" y="8.6" width="3.6" height="3.6" rx="1.8"/>
</svg>
<span class="key__shade" data-shade></span>
</span>
<span class="key__front" data-front>
<span class="key__name">Library</span>
<span class="key__gloss" data-gloss></span>
</span>
</span>
</button>
<button class="key" type="button" data-key data-autoclic>
<span class="key__body" data-body>
<span class="key__top" data-top>
<svg class="key__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="key__fill" d="M13.6 3.6h3.2v16.8h-3.2z"/>
<path d="M3.6 10.4v3.2M6.8 7.6v8.8M10 5.2v13.6M18.8 9.2v5.6M21.2 11v2"/>
<path d="M13.6 3.6h3.2v16.8h-3.2zM15.2 3.6V2.4M15.2 21.6v-1.2"/>
</svg>
<span class="key__shade" data-shade></span>
</span>
<span class="key__front" data-front>
<span class="key__name">Edit</span>
<span class="key__gloss" data-gloss></span>
</span>
</span>
</button>
<button class="key" type="button" data-key>
<span class="key__body" data-body>
<span class="key__top" data-top>
<svg class="key__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="key__fill" d="M5.6 10.4h12.8v9.6H5.6z"/>
<path d="M8.8 10.4H6.4a.8.8 0 0 0-.8.8v8.4a.8.8 0 0 0 .8.8h11.2a.8.8 0 0 0 .8-.8v-8.4a.8.8 0 0 0-.8-.8h-2.4"/>
<path d="M12 14.4V3.2M8.4 6.4 12 2.8l3.6 3.6"/>
</svg>
<span class="key__shade" data-shade></span>
</span>
<span class="key__front" data-front>
<span class="key__name">Share</span>
<span class="key__gloss" data-gloss></span>
</span>
</span>
</button>
<button class="key" type="button" data-key data-autoclic>
<span class="key__body" data-body>
<span class="key__top" data-top>
<svg class="key__icon" viewBox="0 0 24 24" aria-hidden="true">
<rect class="key__fill" x="4.4" y="12.2" width="3.2" height="4.4" rx="0.8"/>
<rect class="key__fill" x="10.4" y="6.2" width="3.2" height="4.4" rx="0.8"/>
<rect class="key__fill" x="16.4" y="10.2" width="3.2" height="4.4" rx="0.8"/>
<path d="M6 3.2v9M6 16.6v4.2M12 3.2v3M12 10.6v10.2M18 3.2v7M18 14.6v6.2"/>
<rect x="4.4" y="12.2" width="3.2" height="4.4" rx="0.8"/>
<rect x="10.4" y="6.2" width="3.2" height="4.4" rx="0.8"/>
<rect x="16.4" y="10.2" width="3.2" height="4.4" rx="0.8"/>
</svg>
<span class="key__shade" data-shade></span>
</span>
<span class="key__front" data-front>
<span class="key__name">Settings</span>
<span class="key__gloss" data-gloss></span>
</span>
</span>
</button>
</div>
<span class="deck__lip" aria-hidden="true"></span>
</nav>
</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 interlocking keys tab bar” from Animorio and fit it into my siteFor each key one thing is worked out: how far down the tip of its lug has gone, in pixels, with its speed. A spring already squeezed a little pushes it up (SPRING), the finger pushes it down with a steady force (FORCE) while it is on it, one stop holds it at the top and one at the bottom. Against the top stop a key let go bounces (BOUNCE) and takes a knock that makes it shiver: a small turn on a quick, damped spring (SHIVER).
The bar is a body of its own, with its mass (HEAVY) and its spring (BAR) pulling it against its stop. The slanted tip of the lug and the tooth of the bar are a constraint between the two: the bar must be pushed aside at least as far as the depth of the lug says. When it is not, the correction is shared between the key and the bar by their masses and the slope of the tip (as in position based dynamics): the key slows down, the bar sets off, and the speed of both is the real displacement over the time.
No sequence is written down: a latched key is let go when the bar moves further than the tooth goes into its notch; a key is caught when its notch has come down to the tooth and the bar has come back. So press, the bar slides, the old one is freed and the new one locks happen by themselves, in that order, and the knobs change the timing but not the order. A finger let go only comes off once the key is caught: even a very quick tap goes all the way down.
Each key is a top and a front folded at 50 degrees, in a 3D context: perspective, rotateX to see it from above, then a turn about a pivot 40 pixels behind the top, in proportion to the depth (TILT). The light comes from a lamp above and a little behind: how far each face is turned to it sets the shade on the plastic and the brightness of the aluminium, and the glint on the front runs. The slot is an SVG: the bar with its teeth and the zigzag spring, which stretches, drawn again on every frame.
Steps of at most 1/240 of a second, frames of at most 1/30, four rounds of constraints per step. A fast key that would jump the slant in a single step is put back on it. When nothing moves any more the loop stops working and drawing. The tabs are buttons in a nav, the chosen one gets aria-current="page" as soon as it is pressed.
| Name | File | Default | What it changes |
|---|---|---|---|
START | script.js | 1 | The key held down when the page opens (0 is the first one). |
FORCE | script.js | 26000 | How hard the finger presses a key: more, it goes down quicker and throws the bar aside harder. |
SPRING | script.js | 320 | How stiff each key's spring is: more, a key let go snaps back up quicker. |
BAR | script.js | 600 | How stiff the bar's spring is: more, it comes back and catches quicker, but the key has a harder job pushing it. |
HEAVY | script.js | 2.5 | How heavy the bar is next to a key: more, it moves slower and resists more. |
BOUNCE | script.js | 0.32 | How much a key coming back up bounces off its stop (0 not at all). |
SHIVER | script.js | 1 | How much a key shivers when it hits a stop (0 not at all). |
TILT | script.js | 11 | How many degrees a key tilts at the bottom of its travel. |
HOVER | script.js | 0.1 | How far a key goes down under the mouse, as a part of its travel. |
For the bottom bar of an app made for phones, with four or five sections, when the app has to do with sound or with things of the past: a voice recorder, a podcast app, a music player, a radio or tape app. The key held down says where you are even standing still.
The plate is at most 420 pixels wide and on a phone narrows to the screen with a 16 pixel margin each side (358 on a 390 phone); it stays 72 tall and each key is a target of about 68 by 72 pixels, with all five names in full. A tap presses the key all the way down even if it is very quick; without a mouse there is no going down a hair. Vertically the page still scrolls (touch-action: pan-y).
No travel, no sliding bar, no bounces or shivers: the faces of the old key and the new one fade for a moment and come back with the new one down and latched, its icon lit, and the old one up. On arrival the plate only fades in, with the key already down.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 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.