
The liquid glass button
Buttons, WebGL, Hover, Physics
NewFree
The firmness of a mattress chosen on a long cushion of night blue velvet filled with memory foam: the chosen option is the one pressed into the cushion. Choosing another, the press slides over to it, and where it has been the foam fills back in slowly, like real memory foam: for a moment the path stays printed in the velvet. The choice changes the cushion itself: soft, it sinks deeper and takes its time to come back; firm, it barely gives and springs back with a small bounce.
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 memory foam picker</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 firmness picker of a mattress shop, made as a long velvet cushion of memory foam. The chosen firmness is
where the cushion is pressed in; choosing another, the press slides across the cover to it and the foam it
leaves fills back in slowly, as memory foam does. The choice changes the cushion itself: soft, it sinks deep
and takes its time to recover; firm, it barely gives and springs back. The cushion is drawn by a WebGL shader
under four real radio buttons, and the letters soften with the foam -->
<div class="firm" data-firmness>
<div class="firm__head">
<span class="firm__title" id="firm-title">Firmness</span>
<span class="firm__hint" aria-live="polite" data-hint>For most sleepers</span>
</div>
<div class="firm__bed" role="radiogroup" aria-labelledby="firm-title" data-bed>
<canvas class="firm__canvas" aria-hidden="true" data-canvas></canvas>
<span class="firm__dent" aria-hidden="true" data-dent></span>
<label class="firm__opt" data-opt data-say="For side sleepers">
<input class="firm__radio" type="radio" name="firmness" value="soft">
<span class="firm__name">Soft</span>
</label>
<label class="firm__opt" data-opt data-say="For most sleepers">
<input class="firm__radio" type="radio" name="firmness" value="medium" checked>
<span class="firm__name">Medium</span>
</label>
<label class="firm__opt" data-opt data-say="For back sleepers">
<input class="firm__radio" type="radio" name="firmness" value="firm">
<span class="firm__name">Firm</span>
</label>
<label class="firm__opt" data-opt data-say="For stomach sleepers">
<input class="firm__radio" type="radio" name="firmness" value="extra-firm">
<span class="firm__name">Extra firm</span>
</label>
</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.
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 memory foam picker” from Animorio and fit it into my siteThe cushion is a role radiogroup with aria-labelledby on the word Firmness, and inside it four labels with their radio input, hidden but present: it takes the focus, the arrows move it as in any radio group (from one end round to the other too) and screen readers read the name. The line above is aria-live polite, so after the choice who it is for is heard as well. The focus ring goes round the option, inside the cushion, only with the keyboard. Under the labels lies a canvas bigger than the cushion, for its shadow on the page.
A shader draws a height field: a capsule (the distance to the edge, negative inside) with a puffy profile, steep at the seam and round on top, and a piping cord 2.4 pixels in radius along the edge. From the height minus the foam pressed in, the slope is taken with central differences, and from that the normal. The light comes from the top left and wraps round as on cloth; the velvet darkens seen face on and glows where the surface turns away, more on the light's side; the crushed pile at the bottom of the hollow is paler, and two soft noises give the pile a slightly different lie here and there.
The foam is a row of 256 depths along the cushion, worked out on every frame: each point fills back by being multiplied by exp(−dt/memory), and the press and the hand push it down again with a maximum. The memory goes from MEMORY seconds (soft) to about a seventh of a second (firm), but it follows the firmness with a 0.9 second lag: the groove left by a soft press stays soft. Before being drawn the row is blurred over nine points and goes to the shader as a 256 by 1 pixel texture, two bytes per point (LUMINANCE_ALPHA): the slope comes out smooth even in shallow hollows.
The press runs to the chosen option on a spring (65 times GLIDE squared, damping 0.72: it goes a little past and comes back), and while it runs it lifts by up to 28%. Its depth is a second spring that is fully damped when soft and bounces when firm (frequency from 10 to 24, damping from 1 to 0.32). In front of it the cover bunches up in a ridge that grows with the speed (PLOW). The firmness of the cushion is a spring too: the depth, width and sharpness of the hollow, how puffy the cushion is and the memory all follow it. The sums run on the GSAP ticker in steps of 1/240 of a second, with the frame capped at 1/30.
The true slopes of a hollow a few pixels deep barely show: in the shader the hollow counts 1.7 times and the normals 1.35. Then from every point the shader walks towards the light in seven steps of 2.2 pixels over the height field: if the rim of the hollow is above the ray, the point is in shadow, softened with the distance. So the rim on the light's side throws a soft shadow into the hollow.
Each name sinks with the foam under it (2.4 pixels in the deepest hollow) and lights up (opacity, and weight from 470 to 640) the nearer and deeper the press is, so it lights up on arrival; with the mouse near it brightens a little. The firmness turns the SOFT axis of Fraunces from 100 to 0. When everything is still and the groove has filled, the loop leaves the ticker and draws nothing more. Without WebGL the cushion is a velvet pill in CSS with a sliding hollow; if the browser takes the WebGL context away, it is built again when it comes back. The library's preview clicks the option after the chosen one, which has data-autoclic.
| Name | File | Default | What it changes |
|---|---|---|---|
START | script.js | 1 | The option chosen when the page opens (0 is the first). |
FIRM | script.js | [0, 0.38, 0.7, 1] | How firm each option is, from 0 (the softest foam) to 1 (the hardest). |
DEPTH | script.js | 9 | How deep the softest option is pressed in, in pixels (the firmest, half as deep). |
MEMORY | script.js | 1.6 | The seconds the softest foam takes to fill back in where the press has been (firmer ones are quicker). |
GLIDE | script.js | 1 | How fast the press slides to the new option (more, quicker). |
PLOW | script.js | 0.6 | How much the cover bunches up in front of the sliding press. |
TOUCH | script.js | 0.3 | How far the mouse, or a finger, sinks in compared with the chosen option's press. |
SHEEN | script.js | 0.85 | How much the velvet glows where the surface turns away from the light. |
SOFTEN | script.js | true | The letters soften with the foam, on the typeface's SOFT axis. |
data-say | index.html | For side sleepers, For most sleepers… | The line saying who each option is for, shown at the top right when it is chosen. |
For a choice that is about softness or comfort: the firmness of a mattress, a pillow or a topper, the padding of a sofa, the cushioning of a running shoe. The hollow says at once what is chosen, and the cushion changing says what changes with the choice. It suits a calm product page, with three, four or five options with short names.
The cushion stays 56 pixels tall and narrows to the screen: at 390 pixels it is 358 wide, the options 89 and the names go down to 15 pixels; the canvas with the cushion and its shadow never leaves the screen. A finger touching the cushion sinks in where it touches, like the mouse pressed, and on the tap the press slides to the new option; no mouse trail, which a finger does not have.
No sliding press and no groove: the old hollow fills back in a tenth of a second while the new one sinks in its place in 0.3 seconds, so the shading moves from one option to the other as a fade; the new name lights up with it. The firmness changes at once, the names do not sink, the mouse does not press in and the line above changes with a fade. When the page opens the cushion fades in already pressed.
GSAP 3.15.0, Fraunces (font variabile, con l'asse SOFT) 5.3.0.
Compressed with gzip, the three demo files weigh 11 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.