
The liquid glass button
Buttons, WebGL, Hover, Physics
NewFree
The Add to bag button of a boutique, covered in real bordeaux velvet: the pile has a direction, and wherever the pointer passes it bends and leaves a lighter or darker trail, like a hand on a velvet sofa, which stays a moment and then slowly rises again. The pile is a field of directions brushed by the pointer and lit fibre by fibre in WebGL, with a sheen that changes with the angle of the pile. On a click a wave combs the whole velvet one way and the words become Added.
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 velvet button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/opsz.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the velvet button: the Add to bag button of a boutique, covered in real velvet. The pile has a direction: where
the pointer passes it bends and leaves a lighter or darker trail that slowly relaxes. A click sends a wave
that combs the whole pile one way, the words say Added, then the velvet is smoothed back. The pile is a field
of directions brushed by the pointer and lit by a WebGL shader; the words stay in HTML, on top -->
<div class="velvet" data-velvet>
<button class="velvet__btn" type="button" data-btn data-autoclic>
<canvas class="velvet__cloth" aria-hidden="true" data-cloth></canvas>
<span class="velvet__face" data-face="rest">
<svg class="velvet__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5.2 8.6h13.6l-1.1 11.1a1.2 1.2 0 0 1-1.2 1.1H7.5a1.2 1.2 0 0 1-1.2-1.1z"/>
<path d="M9 10.8V7.2a3 3 0 0 1 6 0v3.6"/>
</svg>
<span>Add to bag</span>
</span>
<span class="velvet__face" data-face="done" aria-hidden="true">
<svg class="velvet__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5.2 8.6h13.6l-1.1 11.1a1.2 1.2 0 0 1-1.2 1.1H7.5a1.2 1.2 0 0 1-1.2-1.1z"/>
<path d="M9 8.6V7.2a3 3 0 0 1 6 0v1.4"/>
<path d="M9.4 14.6l1.9 1.9 3.5-3.7" pathLength="1" data-check/>
</svg>
<span>Added</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.
18 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 velvet button” from Animorio and fit it into my siteThe script keeps, every 2 pixels of the button, a vector: the way the fibre leans, and its length says how far it is laid (0 upright, 1 flat). At rest they all lean down by NAP, the way the velvet is woven. The field lives in two Float32Arrays and on every frame it changes it goes up to the GPU as a tiny texture, two bytes per cell, which the graphics card blends from one cell to the next.
Every pointer move (with getCoalescedEvents, so a fast hand does not skip) is a segment: the cells near it are pulled towards the segment's direction, LAY long, by a flat bell of the distance from the segment (e to the minus q to the fourth), so the trail has a clean edge. The grip is 1 minus e to the minus length/5: a long stroke lays the pile all the way, a twitch only bends it. The mouse grazes (0.85), the pressed mouse and the finger press flat and are wider; a press opens the pile out round the point.
On every frame each cell goes back towards the lie of the velvet (woven down, or combed by the wave) with an exponential. A cell just brushed keeps the mark, though: it has a hold value that empties in a third of RELAX and brakes the return while it lasts, so the trail stays still for a moment and then goes. When every cell is still and nothing moves, nothing is drawn.
The shader draws the padding from the distance to a rounded rectangle (a slope over the last 12 pixels and a slight dome from top to bottom) and takes its normal. The fibre is the normal tilted by the field. The sheen is the anisotropic one of hair (Kajiya and Kay): the sine of the angle between the fibre and the vector halfway between light and eye, to the tenth. The tone comes from the way the fibre leans: away from the lamp it shows its lit side, towards the lamp its dark tip, upright it lets you see the bottom of the pile. On the edges, where the velvet is seen side on, a grazing light; every fibre is bent a different hair, pixel by pixel, which makes the glitter of velvet.
The fine streaks along the pile are a noise stretched along one of eight fixed directions, mixing the two nearest to the way the pile leans. The field is read through a small noise displacement, so the edges of the trail are ragged, and a slow noise makes the pile lean a little differently from place to place: real velvet is never even.
On the click GSAP widens a ring from the point pressed (from the middle with the keys and in the preview) in WAVE seconds. In the shader the crest is a ridge with a small ripple behind it (a cosine under a bell) that bends the normal and throws the fibres outwards; in the script, behind the crest, every cell is combed the COMB way. When the crest passes the middle the words change; DONE seconds later a softer wave from the right edge puts the velvet back at rest. The button is a real button with aria-live and aria-busy, Enter and Space press it from the middle, the focus ring shows only with the keys. Without WebGL it stays a velvet painted with CSS gradients.
| Name | File | Default | What it changes |
|---|---|---|---|
BRUSH | script.js | 11 | How wide the hand is that brushes the pile, in pixels from its middle. |
LAY | script.js | 0.85 | How far the hand lays the pile down: 0 not at all, 1 flat. |
RELAX | script.js | 6 | The seconds a brushed trail takes to rise back into the velvet. |
NAP | script.js | 0.4 | How far the pile leans down at rest, the way the velvet is woven. |
SHEEN | script.js | 1 | How bright the sheen on the fibres is. |
GRAIN | script.js | 1 | How much grain the cloth has: 0 smooth, 2 coarse. |
COMB | script.js | 25 | The way the click's wave combs the pile, in degrees: 0 to the right, 90 down. |
WAVE | script.js | 0.9 | The seconds the wave takes to cross the whole button. |
DONE | script.js | 2.2 | The seconds the button says Added before the velvet is smoothed back. |
For the main button of a product page in a fashion, jewellery, perfume or furniture shop, or any brand that sells the feel of a material: a single button, on a calm dark background, where people want to touch it. The colour of the velvet changes in the CSS (bottle green, midnight blue, black). Not for a grid of products with a button each.
320 pixels wide, or the screen minus 48, and 60 tall. A finger dragged across brushes the pile like the pressed mouse (touch-action: pan-y, so up and down the page still scrolls); if the finger has moved more than 10 pixels it is not a click. A short tap is the click, with the wave starting where the finger touched. The pixel density is capped at 2.
The velvet stays still: the pointer leaves no trails, a press does not sink in and there is no hand on arrival. On the click the velvet turns to its combed shade in a half second fade, with no wave, and goes back the same way; the words change in a fade, without sliding, and the tick appears already drawn.
GSAP 3.15.0, Bodoni Moda (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.