
The cookie banner
Consent, Transitions, SVG, Physics
NewFree
The courses of a woodworking school in four tabs, and under the tabs the glass vial of a spirit level: the chosen tab is the one holding the bubble between its two marks. Choosing another, the vial tips, the bubble runs uphill stretching out, brakes squashing up and jiggles between the new marks while the vial levels again; the new text slides down the same slope and comes level with it.
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 spirit level tabs</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the courses of a woodworking school, in four tabs. Under the tabs lies the vial of a spirit level: the chosen
tab is the one whose two marks hold the bubble. Choosing another, the far end of the vial sinks, the bubble
runs uphill to the new tab, stretching as it goes, and the vial settles level again; the new text slides in
down the same slope and levels out with it. The vial and the bubble are drawn by the script in the SVG -->
<div class="level" data-level>
<div class="level__row">
<div class="level__tabs" role="tablist" aria-label="Courses" data-tabs>
<button class="tab" type="button" role="tab" id="tab-joinery" aria-controls="page-joinery" aria-selected="true"><span class="tab__name">Joinery</span></button>
<button class="tab" type="button" role="tab" id="tab-turning" aria-controls="page-turning" aria-selected="false" tabindex="-1"><span class="tab__name">Turning</span></button>
<button class="tab" type="button" role="tab" id="tab-carving" aria-controls="page-carving" aria-selected="false" tabindex="-1"><span class="tab__name">Carving</span></button>
<button class="tab" type="button" role="tab" id="tab-finishing" aria-controls="page-finishing" aria-selected="false" tabindex="-1"><span class="tab__name">Finishing</span></button>
</div>
<div class="vial" aria-hidden="true" data-vial>
<svg class="vial__glass" data-glass>
<defs>
<linearGradient id="vial-liquid" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: var(--liquid-edge)"/>
<stop offset="0.1" style="stop-color: var(--liquid-deep)"/>
<stop offset="0.26" style="stop-color: var(--liquid-light)"/>
<stop offset="0.4" style="stop-color: var(--liquid)"/>
<stop offset="0.7" style="stop-color: var(--liquid-deep)"/>
<stop offset="0.86" style="stop-color: var(--liquid-light)"/>
<stop offset="0.95" style="stop-color: var(--liquid-deep)"/>
<stop offset="1" style="stop-color: var(--liquid-edge)"/>
</linearGradient>
<linearGradient id="vial-air" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: var(--air-shade)"/>
<stop offset="0.3" style="stop-color: var(--air)"/>
<stop offset="0.72" style="stop-color: var(--air)"/>
<stop offset="1" style="stop-color: var(--air-glow)"/>
</linearGradient>
<linearGradient id="vial-rim" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color: var(--rim-dark)"/>
<stop offset="0.22" style="stop-color: var(--rim)"/>
<stop offset="0.78" style="stop-color: var(--rim)"/>
<stop offset="1" style="stop-color: var(--rim-dark)"/>
</linearGradient>
<linearGradient id="vial-cap" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: var(--brass-dark)"/>
<stop offset="0.22" style="stop-color: var(--brass-light)"/>
<stop offset="0.5" style="stop-color: var(--brass)"/>
<stop offset="1" style="stop-color: var(--brass-dark)"/>
</linearGradient>
<radialGradient id="vial-glow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" style="stop-color: var(--liquid-light); stop-opacity: 1"/>
<stop offset="1" style="stop-color: var(--liquid-light); stop-opacity: 0"/>
</radialGradient>
<clipPath id="vial-inside"><rect data-inside/></clipPath>
</defs>
<rect class="vial__tube" data-tube/>
<rect class="vial__liquid" fill="url(#vial-liquid)" data-liquid/>
<g clip-path="url(#vial-inside)">
<g data-bubble>
<ellipse class="vial__glow" fill="url(#vial-glow)" data-glow/>
<path class="vial__air" fill="url(#vial-air)" stroke="url(#vial-rim)" data-air/>
<path class="vial__shine" data-shine/>
</g>
</g>
<g class="vial__marks" data-marks></g>
<rect class="vial__gleam" data-gleam/>
<rect class="vial__cap" fill="url(#vial-cap)" data-cap="0"/>
<rect class="vial__cap" fill="url(#vial-cap)" data-cap="1"/>
<line class="vial__ring" data-ring="0"/>
<line class="vial__ring" data-ring="1"/>
</svg>
</div>
</div>
<div class="level__pages" data-pages>
<section class="page" role="tabpanel" id="page-joinery" aria-labelledby="tab-joinery" tabindex="0">
<h2 class="page__title">Dovetails by hand</h2>
<p class="page__text">Mark, saw and chop the half-blind dovetails of a small cherry drawer, then glue it up square and take it home.</p>
<dl class="page__facts">
<div><dt>Length</dt><dd>Two days</dd></div>
<div><dt>Group</dt><dd>Six people</dd></div>
<div><dt>Price</dt><dd>€240</dd></div>
</dl>
</section>
<section class="page" role="tabpanel" id="page-turning" aria-labelledby="tab-turning" tabindex="0" hidden>
<h2 class="page__title">A bowl from green wood</h2>
<p class="page__text">Rough out a wet ash log on the lathe, hollow it with a bowl gouge and leave it to dry slowly in a paper bag.</p>
<dl class="page__facts">
<div><dt>Length</dt><dd>One day</dd></div>
<div><dt>Group</dt><dd>Four people</dd></div>
<div><dt>Price</dt><dd>€150</dd></div>
</dl>
</section>
<section class="page" role="tabpanel" id="page-carving" aria-labelledby="tab-carving" tabindex="0" hidden>
<h2 class="page__title">Spoons from a birch log</h2>
<p class="page__text">Split the blank with an axe, shape it on the chopping block and finish the bowl of the spoon with a hook knife.</p>
<dl class="page__facts">
<div><dt>Length</dt><dd>One day</dd></div>
<div><dt>Group</dt><dd>Eight people</dd></div>
<div><dt>Price</dt><dd>€110</dd></div>
</dl>
</section>
<section class="page" role="tabpanel" id="page-finishing" aria-labelledby="tab-finishing" tabindex="0" hidden>
<h2 class="page__title">Oil, wax and shellac</h2>
<p class="page__text">Scrape and burnish a walnut box, then build up a French polish with a cotton rubber until it shines like glass.</p>
<dl class="page__facts">
<div><dt>Length</dt><dd>Half a day</dd></div>
<div><dt>Group</dt><dd>Eight people</dd></div>
<div><dt>Price</dt><dd>€80</dd></div>
</dl>
</section>
</div>
</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.
60 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 spirit level tabs” from Animorio and fit it into my siteThe tabs are four equal columns, each with its name on top and its stretch of the vial below, so the whole column can be tapped. The vial is an SVG over the columns that takes no clicks: the tube, the liquid with a vertical gradient (dark edges, the light the round glass gathers at the top and bottom), a line of reflection, the brass caps and a pair of marks per tab, measured from the width of the columns and redone by a ResizeObserver.
The bubble has a place and a speed. A dip under the chosen tab pulls it with a spring, but only as hard as the slope allows: the cap is a small base push plus a push in proportion to the sine of the tilt (both times RUSH), so the bubble sets off as the vial tips. The liquid holds it back with a drag in proportion to its speed, much stronger near the marks: that is what decides how far past it goes (WOBBLE). The sums run on the GSAP ticker in steps of 1/240 of a second, with the frame capped at 1/30.
The tilt is a stiff spring aiming at TILT degrees towards the new tab, a little more for a far one, and back to zero once the bubble is within four tenths of a tab. The vial turns with rotation and drops by half its length times the sine of the angle: in effect it turns about its high end, so nothing rises into the names. The mouse adds a second, small tilt (LEAN), which moves the bubble a few pixels.
The shape has a spring of its own: it wants to be longer the faster the bubble goes (STRETCH), with its area kept, and when the bubble brakes the spring overshoots and squashes it. The outline is a squared-off ellipse of 44 points, worked out on every frame, with its back half narrowing with the speed like a drop; on top is a highlight and below it a paler light in the liquid. A tab's marks darken when the bubble is near.
The old lines leave in 0.13 seconds towards the low side; the new ones come in from the high side with power3.out in TEXT seconds, one after the other. Each line has a spring that follows the tilt of the vial, softer for the lower ones, and turns about its bottom edge: so the text arrives crooked and straightens with the vial. The component is held from the top of the page and the height of the text changes with a tween.
It is a real tablist: role tab and tabpanel, aria-selected and a single tab reachable with the Tab key; the arrows, Home and End change tab and bring the focus along. When bubble, vial, shape and lines are all still, the loop takes itself off the ticker and draws nothing until something happens. The library's preview clicks the tab after the chosen one, which has data-autoclic.
| Name | File | Default | What it changes |
|---|---|---|---|
START | script.js | 0 | The tab chosen when the page opens (0 is the first). |
TILT | script.js | 1.3 | How far the vial tips towards the new tab, in degrees (a little more for a far one). |
RUSH | script.js | 1 | How fast the bubble runs through the liquid: more, a quicker bubble. |
WOBBLE | script.js | 0.5 | How far the bubble overshoots and jiggles when it arrives (0 stops softly, 1 very lively). |
STRETCH | script.js | 0.5 | How much the bubble stretches as it runs and squashes as it brakes. |
LEAN | script.js | 0.35 | How far the vial tips towards the tab under the mouse, in degrees. |
TEXT | script.js | 0.55 | The seconds the new text takes to come in. |
For three, four or five tabs with short names, on a site that has to do with hands and tools: a woodworking school, a tool shop, an architecture studio, a measuring app. Even still, the bubble between its two marks says at once where you are.
The component is at most 560 pixels wide and on a phone narrows to the screen, with a 16 pixel margin each side: the four columns become 89 pixels, the bubble shortens to 34 and the row of tabs stays 60 tall, name and vial together, all of it tappable. The new text comes in from closer by, so it does not leave the screen. With a finger down on a tab the vial tips towards it as with the mouse over it.
The vial does not tip and the bubble does not run: it fades out between the old marks and back in between the new ones; the new text fades in, already straight. When the page opens everything is already in place, with no tilt under the mouse and no knock.
GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 9.7 KB; the libraries: GSAP 28 KB.
Tested on 5 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.