
The layered button
Buttons, Hover, Physics
Free
The bottom bar of a neighbourhood café's app: over the chosen tab hangs a small enamel sign with its name, on two cords from a thin brass rail. Changing tab, the hooks run along the rail and the sign follows late: it trails, tilts, swings past and settles like a real rigid body, then the icon under it hops and gives it a knock.
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 shop sign tab bar</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the tab bar of a neighbourhood café's app, on a cream board. Along its top runs a thin brass rail, and from
two hooks on it hangs a small enamel sign with the name of the tab you are on, like the plate over a shop
door. Choosing another tab, the hooks run along the rail and the sign follows on its two cords: it trails,
tilts, swings past and settles, and the icon under it hops. The sign is a rigid plate worked out by the
script on every frame -->
<nav class="bar" aria-label="Main" data-bar>
<button class="tab" type="button" data-tab data-autoclic>
<span class="tab__lift" data-lift>
<svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="tab__fill" d="M5.4 4.6h13.2l1.6 4.6H3.8z"/>
<path d="M3.8 9.2 5.4 4.6h13.2l1.6 4.6M8.7 4.6l-.8 4.6M12 4.6v4.6M15.3 4.6l.8 4.6"/>
<path d="M3.8 9.2a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0"/>
<path d="M5.3 12.4v7.4h13.4v-7.4M10.2 19.8v-4.4h3.6v4.4"/>
</svg>
</span>
<span class="sr" data-name>Home</span>
</button>
<button class="tab" type="button" data-tab>
<span class="tab__lift" data-lift>
<svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="tab__fill" d="M5.2 10.2h11.2v3a5.2 5.2 0 0 1-5.2 5.2h-.8a5.2 5.2 0 0 1-5.2-5.2z"/>
<path d="M5.2 9.4h11.2v3.8a5.2 5.2 0 0 1-5.2 5.2h-.8a5.2 5.2 0 0 1-5.2-5.2zM16.4 10.6h1.2a2.3 2.3 0 0 1 0 4.6h-1.5M3.8 20.6h14"/>
<path d="M8.9 3.4c-.9 1.1.9 2.1 0 3.4M12.6 3.4c-.9 1.1.9 2.1 0 3.4" data-steam/>
</svg>
</span>
<span class="sr" data-name>Menu</span>
</button>
<button class="tab" type="button" data-tab data-autoclic>
<span class="tab__lift" data-lift>
<svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="tab__fill" d="M5.6 8.4h12.8l-.9 11.2a1 1 0 0 1-1 .9H7.5a1 1 0 0 1-1-.9z"/>
<path d="M5.6 8.4h12.8l-.9 11.2a1 1 0 0 1-1 .9H7.5a1 1 0 0 1-1-.9zM9.2 10.8V7.4a2.8 2.8 0 0 1 5.6 0v3.4"/>
</svg>
</span>
<span class="sr" data-name>Orders</span>
</button>
<button class="tab" type="button" data-tab>
<span class="tab__lift" data-lift>
<svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="tab__fill" d="M7 4.4h10a.6.6 0 0 1 .6.6v15.2l-5.6-4-5.6 4V5a.6.6 0 0 1 .6-.6z"/>
<path d="M7 4.4h10a.6.6 0 0 1 .6.6v15.2l-5.6-4-5.6 4V5a.6.6 0 0 1 .6-.6z"/>
</svg>
</span>
<span class="sr" data-name>Saved</span>
</button>
<button class="tab" type="button" data-tab data-autoclic>
<span class="tab__lift" data-lift>
<svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="tab__fill" d="M12 4.4a3.7 3.7 0 1 0 0 7.4a3.7 3.7 0 1 0 0-7.4z"/>
<path d="M12 4.4a3.7 3.7 0 1 0 0 7.4a3.7 3.7 0 1 0 0-7.4zM4.8 20.2c.9-3.7 3.7-5.7 7.2-5.7s6.3 2 7.2 5.7"/>
</svg>
</span>
<span class="sr" data-name>Profile</span>
</button>
<!-- the brass rail along the top, and everything that hangs from it: the sign's shadow on the board, the two
cords, the two hooks and the sign. None of it takes the taps -->
<span class="rail" aria-hidden="true" data-rail></span>
<span class="hanger" aria-hidden="true" data-hanger>
<span class="shade" data-shade></span>
<svg class="cords">
<g class="cords__shade" transform="translate(1 2.4)"><path data-cord-shade/><path data-cord-shade/></g>
<g class="cords__line"><path data-cord/><path data-cord/></g>
</svg>
<span class="hook" data-hook></span>
<span class="hook" data-hook></span>
<span class="sign" data-sign>
<span class="sign__plate">
<span class="sign__rule"></span>
<span class="sign__words" data-words></span>
<span class="sign__side" data-side></span>
<span class="sign__gloss" data-gloss></span>
</span>
<span class="sign__clip" data-clip></span>
<span class="sign__clip sign__clip--right" data-clip></span>
</span>
</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.
26 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 shop sign tab bar” from Animorio and fit it into my siteThe sign is its four corners, four points with a position and a speed. Six distance constraints keep them rigid: the four sides and the two diagonals. At every step the points move with their speed and gravity, then the constraints put the positions right (six rounds), and the new speed is the real displacement divided by the time: Verlet integration written with speeds, which takes knocks and pushes well. Steps of at most 1/240 of a second, frames of at most 1/30.
Each cord is a constraint that only acts when taut: if the point it is tied to is further from the hook than the cord is long, the plate is pulled back. The tie point lies inside the plate, so it is a blend of three corners, and the correction is shared between them by their weight in the blend. The cords lean in a little (TOE): with parallel cords the plate would slide along level like a swing, this way it rocks as it swings, like a pendulum hung from a point above the rail.
GSAP carries the hooks: one tween moves their middle with power2.inOut, another runs the front one a few pixels ahead and brings it back. Within a frame the hooks move a little at every step of the sums, so even at 30 frames a second the plate gets no jolts. The damping works on the plate's speed relative to the hooks': it slows the swinging, not the ride.
Turning towards a tab is the twist of a sign hung on two strings (a bifilar pendulum), with its own spring: the stiffness comes from gravity, the distance between the cords, their length and the sign's width. The sign is a real HTML element (its text stays sharp) moved with translate, rotate and perspective plus rotateY; the cords are two SVG paths to the clips, worked out with the same perspective, straight when taut and bowed outwards when slack; the shadow on the board follows the plate.
When the sign arrives, the icon under it hops with a GSAP timeline; while it rises it is an obstacle the solver knows about: if the plate's lower edge is inside it, it is pushed up, and the plate jumps on its own. The other way round, if the swinging plate comes down on an icon, the icon is pushed down and tilted on the side pressed, and comes back on a spring. The tabs are buttons in a nav, the chosen one has aria-current="page"; when nothing moves any more the loop stops working and drawing.
| Name | File | Default | What it changes |
|---|---|---|---|
START | script.js | 1 | The tab chosen when the page opens (0 is the first one). |
TRAVEL | script.js | 0.46 | The seconds the hooks take to reach the next tab (a far one takes a little more). |
LEAD | script.js | 5 | How many pixels the front hook runs ahead of the back one on the way: the sign tilts. |
CORD | script.js | 7 | The length of the two cords, in pixels. |
TOE | script.js | 3.5 | How far the cords lean in towards each other, in pixels: more, the sign rocks more as it swings (0, it slides along level). |
GRAVITY | script.js | 2400 | How heavy the sign is, in pixels per second squared: more, quicker and smaller swings. |
DAMPING | script.js | 2.6 | How soon the swinging dies away: more, it stops sooner. |
TURN | script.js | 30 | How many degrees the sign turns towards a tab under the mouse or the finger. |
HOP | script.js | 6 | How high the icon hops when the sign arrives over it, in pixels (above about 5 it knocks it). |
For the bottom bar of an app made for phones, with four or five sections, when the app has the feel of a shop: a café, a bookshop, a bakery, a corner store, a recipe app. Even standing still the sign says where you are, in place of the labels under the icons.
The bar 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 tab is a target of about 69 by 72 pixels. The sign is 60 pixels wide and fits its column there too. With a finger down on a tab the sign turns towards it, as with the mouse over it, and the tap chooses it. Vertically the page still scrolls (touch-action: pan-y).
No ride, no swinging, no twist and no hop: the sign, with its cords and hooks, fades off the old tab and back in, still and level, on the new one with its new name, while the icons light up and go out with a fade. On arrival the bar only fades in, with the sign already hung.
GSAP 3.15.0, Fraunces (font variabile) 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.