
The liquid glass button
Buttons, WebGL, Hover, Physics
NewFree
The sync panel of a photo app, with a coloured cord for every device: as the bytes arrive the three taut cords are plaited from the left into a single braid, and the braid is the progress bar. If a device goes offline its cord comes out of the clamp and lies slack and grey until Retry plugs it back in; at the end the clamp shuts on the braid, a tightening runs back along it and a glint runs forward.
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, viewport-fit=cover">
<title>The sync braid</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/radio-canada@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the sync panel of a photo app: one cord for every device, plaited into a single braid as the bytes arrive.
The braid is the progress bar; the loose cords still running to the clamp on the right are what is left.
The cords are painted by a WebGL shader on the canvas; the words, the clamps and the button are HTML -->
<section class="sync" aria-labelledby="sync-title" data-sync>
<div class="sync__head">
<div class="sync__words">
<h2 class="sync__title" id="sync-title"><span class="sync__swap" data-title>Photos</span></h2>
<p class="sync__meta"><span class="sync__swap" data-meta>Last synced yesterday</span></p>
</div>
<p class="sync__figure" aria-hidden="true">
<span class="sync__pct" data-pct>2.4 GB</span>
<svg class="sync__tick" viewBox="0 0 24 24" data-tick><path d="M5.5 12.6l4.1 4.1L18.6 7.4" pathLength="1"/></svg>
</p>
</div>
<div class="sync__bar" role="progressbar" aria-labelledby="sync-title" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" data-bar>
<canvas class="sync__paint" aria-hidden="true" data-paint></canvas>
<span class="sync__cap sync__cap--start" aria-hidden="true"></span>
<span class="sync__cap sync__cap--end" aria-hidden="true" data-clamp></span>
</div>
<div class="sync__foot">
<p class="sync__devices" data-devices></p>
<button class="sync__btn" type="button" data-action data-autoclic><span class="sync__label" data-label>Sync now</span></button>
</div>
<p class="sr" role="status" aria-live="polite" data-live></p>
</section>
</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 sync braid” from Animorio and fit it into my siteThe cords are not drawn as strokes: for every column of pixels of the canvas the script works out how high each of the three cords passes and how deep it is, and writes the numbers into a small texture three rows tall (the height in two bytes, to a 256th of a pixel, the depth in one, plus a byte that says whether the cord is there at all). Every frame the texture is updated with texSubImage2D: a few hundred columns and no geometry.
For every pixel the shader looks for the nearest point of each cord among the columns round it (the segments from one column to the next) and takes its distance d. Within the radius r the cord is a tube: its height there is the strand’s depth plus the round of the tube, √(r² − d²); the highest tube wins, and the three are laid down from the lowest to the highest with an edge softened over half a pixel. So the crossings of the braid are exact to the pixel, even where two cords touch at a slant. The tube’s normal gives the diffuse light and the highlight, slanted plies twist round every cord, and the shadows fall down and to the right on the panel and on the cord underneath, the further the higher the cord is.
Strand k follows y = A sin θ and z = sin 2θ, with θ = 2π (x ÷ P + k ÷ 3): seen from the front it is the classic plait, and the double sine of the depth takes every strand once over and once under the others. Up to the tip (the part that has arrived) the strands follow the braid; past it, over a stretch a little longer than one turn, they open out towards their lanes with a smootherstep and the depth fades with them. The tip follows the bytes on a critically damped spring that never goes back.
The loose stretch of every cord is a row of 48 points that moves sideways by the wave equation, with a pull towards the curve it should lie in and some damping, worked out in steps of 1/480 of a second. The tension changes everything: taut, the waves run fast and the cord is pulled straight; slack, the waves are lazy and the cord goes back to the curve it remembers (the two outer ones bow outwards, the middle one into an S). Pulled taut at once, it rings: that is the twang. While the devices are compared one pulse at a time sets off, with the position and the speed of a wave going left, w = g(x + ct), so that it runs one way only; while plaiting, every crossing gives the cords a little push.
When a device goes offline its cord comes away: its end springs back with back.out, it becomes free (the string has a mirrored ghost point instead of a held end) and the shader greys it only past the tip of the braid. Retry takes it back into the clamp, gives it its colour back and pulls it taut. At the end the clamp shuts with an elastic squash while the lanes close in on the braid; then a tightening, the braid’s width dipping under a bell, runs from right to left, and a glint from left to right.
The bar is a role=progressbar with aria-valuenow and an aria-valuetext in gigabytes, the panel has aria-busy while it works and an aria-live line says when the sync starts, stops, loses a device and ends. The button changes its word (Sync now, Pause, Resume, Retry, Sync again) and the pill takes the new width with a little bounce; a click from a script, the library preview’s, never pauses. Without WebGL the cords become three stripes drawn by the CSS, and everything else works.
| Name | File | Default | What it changes |
|---|---|---|---|
SECONDS | script.js | 5.5 | How long a sync takes in the demo, in seconds (a real one follows its bytes). |
SIZE | script.js | 2.4 | The gigabytes to sync. |
DEVICES | script.js | ['Laptop', 'Tablet', 'Phone'] | The three devices, one per cord, from the top. |
FAIL_EVERY | script.js | 2 | In the demo, one sync in how many goes wrong (0 never). |
FAIL_AT | script.js | 0.58 | How far the sync that goes wrong gets, from 0 to 1. |
OFFLINE | script.js | 2 | Which device goes offline then (0 the top one, 2 the bottom one). |
CORD | script.js | 9 | How thick a cord is, in pixels. |
PITCH | script.js | 4.2 | How long one turn of the braid is, in cord thicknesses. |
SPREAD | script.js | 1.65 | How far apart the loose cords run, in cord thicknesses. |
TWANG | script.js | 1 | How long a cord rings when it is pulled taut: 0 not at all, 1 a lot. |
FOLLOW | script.js | 0.35 | How long the braid takes to catch up with the bytes, in seconds. |
SHEEN | script.js | 0.8 | The glint that runs along the finished braid: 0 none, 1 bright. |
For a wait of a few seconds that has an outcome: syncing devices, merging libraries, a backup, an import from several sources. The three cords say «three things becoming one» well, and the device that drops out shows at once; for a single file to upload a plainer bar is better.
The panel takes the screen’s width less 32 pixels (358 on a 390 screen) and stays 183 tall; the braid gets shorter with it and the shader paints at the screen’s real density, up to twice. The button is a pill 34 tall: under a finger it squashes and springs back, and its words widen on the width axis only under the mouse.
No twangs, pulses or shudders: the cords go straight from the slack curve to the taut line, the cord that goes offline is out of the clamp at once, the clamp shuts without a squash, a tightening or a glint, and the tick and the new words fade in. The braid still grows, because it is what says how far along things are.
GSAP 3.15.0, Radio Canada (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 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.