Skip to content
animorio
IT
All components

The interlocking keys tab bar

Navigation, Physics, 3DCSS only, GSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

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

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 interlocking keys tab bar” from Animorio and fit it into my site

What it does

On a white background, alone in the middle, a brushed aluminium plate 72 pixels tall with a dark opening holding five long lever keys, seen a little from above: the top of cream plastic with a line icon, the sloping aluminium front with the name engraved in narrow capitals. They are Record (a microphone), Library (a cassette), Edit (a waveform with a cursor), Share (a box with an arrow) and Settings (three mixer faders). The edge of the plate throws a shadow on the backs of the keys, which go on under it to their pivot. Under the opening, in a narrow slot, the works can be seen: the steel latch bar with a hooked tooth for each key, its zigzag spring at the left end and the brass lugs coming down from the keys. On arrival the plate rises with every key up, then the Library key presses itself: its lug comes down, pushes the bar aside with its slanted tip, the bar comes back and catches it, and the key stays down, lower than the others, tilted forward, darker where its neighbours shade it, its icon lit red. Pressing another key everything happens in order, worked out: the new key goes down, its lug pushes the bar, the bar, pushed aside, lets go of the old key's lug, which springs back up, hits its stop, bounces and shivers a little, and its icon goes out; the new lug passes the tooth, the bar comes back on its spring and catches it, and the new key, let go, rises a touch up to the tooth and stops there with a small knock. With the mouse over a key it goes down a hair and its icon darkens. Holding the button down, the key stays at the bottom until it is let go. From the keyboard, Tab puts a ring round a key, Enter or Space press it, and the arrows, Home and End move to another key and press it.

How it works

Each key is a lever on its own spring

For 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 and the slanted tooth

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.

The order comes from the shapes

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.

The drawing in CSS 3D

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.

Working little, then stopping

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.

Values to change

NameFileDefaultWhat it changes
STARTscript.js1The key held down when the page opens (0 is the first one).
FORCEscript.js26000How hard the finger presses a key: more, it goes down quicker and throws the bar aside harder.
SPRINGscript.js320How stiff each key's spring is: more, a key let go snaps back up quicker.
BARscript.js600How stiff the bar's spring is: more, it comes back and catches quicker, but the key has a harder job pushing it.
HEAVYscript.js2.5How heavy the bar is next to a key: more, it moves slower and resists more.
BOUNCEscript.js0.32How much a key coming back up bounces off its stop (0 not at all).
SHIVERscript.js1How much a key shivers when it hits a stop (0 not at all).
TILTscript.js11How many degrees a key tilts at the bottom of its travel.
HOVERscript.js0.1How far a key goes down under the mouse, as a part of its travel.

Try them in the Playground

When to use it

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.

On phones

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

With reduced motion

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.

Pitfalls

  • At rest the keys never stopped: at every step the spring pushes the key against its stop, and that small movement was taken for a knock, with a bounce and a shiver, so the page was never still. Now below 40 pixels a second arriving on the stop is resting on it, not a knock.
  • With the bar's spring too stiff the key stopped halfway, resting on the slant: to push the bar it takes a force equal to its spring times the slope of the tip. The limits of the FORCE, BAR and SPRING knobs are set so the finger always wins.
  • Letting the button go at once, the finger came off before the bar had come back, and the key rose without being caught: the tab changed but no key stayed down. Now a finger let go stays until the key is caught (0.6 seconds at most).
  • Pressing two keys almost together latched both of them, as on a real tape deck, because the bar pushed by the first lets the second through. Only the last one chosen is caught now; the other rises.
  • In reduced motion the fade was on the whole key, and a 3D element that is not fully opaque is flattened: for a moment the front looked squashed. Now the two faces fade.
  • A fast key could jump the whole slant in a single step, and the bar was teleported. The key is now put back on the slant and the constraint sorts it out.

Libraries

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.

Testing

Tested on 8 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