
The layered button
Buttons, Hover, Physics
Free
The Sign out button of an account, painted like the door of a small shop: bottle green, gilded words, and in the round end of the pill the Open / Closed sign hanging from a brass nail on a loop of twine. The sign is a rigid body that swings on the nail and turns on itself; the twine is two strands of verlet points. The pointer brushing past sets it swinging; at a click the sign goes round more than once, the strands twist and lift it, it winds back and forth a couple of times and stops on Closed: Signing out…, then See you soon, Ada, and the door goes dark. The second click turns it back to Open: Welcome back, Ada.
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 button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the shop sign button: the Sign out button of an account, painted like the door of a small shop, with the Open /
Closed sign hanging from a brass nail in its round left end on a loop of twine. The sign is a rigid body that
swings, the twine is two strands of verlet points: the pointer brushing the sign sets it swinging, a click
spins it round on the nail, the strands twist and lift it, it winds back and forth and stops on Closed -->
<div class="shop" data-shop>
<button class="shop__btn" type="button" data-btn data-autoclic>
<span class="shop__fill" aria-hidden="true"></span>
<canvas class="shop__sign" aria-hidden="true" data-sign></canvas>
<span class="shop__words" data-words>
<span class="shop__word" data-word>Sign out</span>
</span>
</button>
<p class="sr" aria-live="polite" data-live></p>
</div>
</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.
29 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 button” from Animorio and fit it into my siteThe sign has two degrees of freedom, worked out in steps of 1/240 of a second with the frame capped at 1/30: the swing θ on the nail, in the plane of the door (θ'' = −ω² sin θ − c θ', ω = 2π · SWING, with a soft stop past 0.26 radians so it never leaves the pill or covers the words), and the turn φ about the vertical through the nail. The twine is the torsion spring: φ'' = −ωt² (φ − φ0) − 2ζ ωt φ', where φ0 is the turn at which the twine is untwisted and ωt = 2π · TWIST.
The click moves φ0 on by half a turn (the loop turns on the nail) and gives the sign a speed. Which speed makes it go exactly TURNS turns before the twine stops it? The script runs the spring forward and finds the speed by bisection (forty tries, a few milliseconds). So the sign always goes a little past, comes back with its damping and stops on the right face, whatever TWIST and DAMPING are.
Each strand has ten points in three dimensions, pinned to the nail and to the eyelet, which comes from the sign (±15 · cos φ across, ±15 · sin φ in depth, turned by θ). Verlet integration with gravity and drag, fourteen rounds of slightly slack length constraints: the strands lag and slacken when the sign moves. When the sign is turned, the points near the nail are pulled onto a helix: the two strands wrap round each other over a length that grows with the twist, and the sign rises because the open V gets shorter (drop = b + √((L − b)² − 15²)).
The two faces are painted once on off-screen canvases at twice the density. Every frame the sign is drawn in 28 vertical strips: each strip has its own depth z = u · sin φ and its own scale f / (f − z), so the near edge is taller than the far one. With cos φ negative the back is used, its columns read the other way. On top, on a layer of its own with source-atop, the shade of turning edge-on and a sheen running across; the thickness is a band 1.7 · |sin φ| wide. The layer is laid on the door with shadowBlur: the shadow follows the true outline.
The pointer's speed is measured between one pointermove and the next. If it is over the sign (in the sign's own frame, turned by θ), the moment of its push about the nail, (r × v) / |r|², becomes an acceleration of θ, plus a small push on φ. The pointer coming onto the pill gives a push from the side it comes from. When swing, turn and twine are still the loop stops working and drawing.
A real button with aria-disabled while it signs out or in, an aria-live region saying Signing out…, Signed out. See you soon, Ada., Signing in… and Signed in. Welcome back, Ada. The pill is as wide as the longest of the six phrases measured already wide (font-stretch 116%), so it never changes size. The old words leave before the new ones come in; the closed door is a second coat that shows through the --shut variable.
| Name | File | Default | What it changes |
|---|---|---|---|
SIGN_OUT | script.js | Sign out | The words while signed in. |
SIGNING_OUT | script.js | Signing out… | The words while it signs out; the aria-live region says it too. |
SIGNED_OUT | script.js | See you soon, Ada | The goodbye once signed out. The name is made up: in a real site it is the user's. |
SIGN_IN | script.js | Sign in | The words while signed out, offering the way back. |
SIGNING_IN | script.js | Signing in… | The words while it signs in. |
SIGNED_IN | script.js | Welcome back, Ada | The welcome once signed in again. |
FRONT | script.js | OPEN | The word on the sign's front, while signed in. If it is long the type gets smaller. |
BACK | script.js | CLOSED | The word on the back, while signed out. |
WAIT | script.js | 1.3 | The seconds of the simulated sign out or sign in. In a real site, your server's answer. |
SAID | script.js | 0.9 | The seconds the goodbye stays before the button offers the way back. |
TURNS | script.js | 1.25 | How many turns the sign goes round at a click before the twine stops it. |
SWING | script.js | 1.15 | The swings a second on the nail: higher a light sign, lower a heavy one. |
TWIST | script.js | 0.72 | How many times a second the sign turns back and forth on the twisted twine. |
DAMPING | script.js | 0.3 | How soon it stops: 0.05 it swings and turns a long time, 1 it stops at once. |
BRUSH | script.js | 1 | How hard the pointer brushing past sets it swinging: 0 it does not touch it. |
For signing out of an account, or in, on a site with a handmade character: an online shop, a café, a bookshop, a bakery, a neighbourhood store; also as an Open / Closed switch in the dashboard of someone running a place (bookings open, orders paused). The sign out has to be wired to your own: here it is all simulated.
The pill is about 270 pixels wide and fits at 390 with room to spare; if the words are longer than the room the type gets smaller. A tap spins the sign; a finger dragged over it sets it swinging.
The sign hangs still and straight, the twine taut; it neither swings nor turns. At a click the OPEN face fades into the CLOSED face (and back), the door darkens and the words change with a fade; the pill does not swell and the words do not widen (under the mouse the fill only lightens a little).
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0, Fraunces (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.
Tested on 12 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.