Skip to content
animorio
IT
All components

The velvet button

Buttons, WebGL, HoverWebGL, GSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

On a black background, on its own, a button 60 pixels tall and 320 wide covered in bordeaux velvet over a padding: the velvet rounds over the edges, where it catches a soft light from the side, and is slightly domed in the middle, lighter at the top towards the lamp. On it, stamped in champagne, the words ADD TO BAG in spaced capitals, in Bodoni, with a small drawn bag. On arrival the velvet fades in, the words close up into place and an invisible hand passes once over the cloth from left to right, leaving a lighter trail. Moving the mouse over it, the pile bends the way it goes: towards the right or downwards the trail is lighter and glossier, towards the left or upwards darker and deeper, with ragged edges like real ones. The trail holds still for a couple of seconds and then slowly rises until it is gone. Pressing, the velvet sinks into the padding and the pile opens out round the finger. On the click a wave starts from the point pressed: its crest, with a small ripple behind it, lifts the pile and throws it outwards, with a thread of light on the side facing the lamp, and behind it the whole velvet stays combed one way, lighter and streaked. When the crest passes the middle the words become ADDED and a tick draws itself in the bag. After 2.2 seconds a softer wave starts from the right edge and smooths the velvet back, and the words go back to ADD TO BAG.

How it works

The pile is a field of directions

The 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.

The hand brushes

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.

The pile slowly rises

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.

Light, fibre by fibre

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 grain

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.

The wave and the words

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.

Values to change

NameFileDefaultWhat it changes
BRUSHscript.js11How wide the hand is that brushes the pile, in pixels from its middle.
LAYscript.js0.85How far the hand lays the pile down: 0 not at all, 1 flat.
RELAXscript.js6The seconds a brushed trail takes to rise back into the velvet.
NAPscript.js0.4How far the pile leans down at rest, the way the velvet is woven.
SHEENscript.js1How bright the sheen on the fibres is.
GRAINscript.js1How much grain the cloth has: 0 smooth, 2 coarse.
COMBscript.js25The way the click's wave combs the pile, in degrees: 0 to the right, 90 down.
WAVEscript.js0.9The seconds the wave takes to cross the whole button.
DONEscript.js2.2The seconds the button says Added before the velvet is smoothed back.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The grain's streaks taken in a frame turning with the pile (the product of the position and the direction) made, far from the top corner, whorls like the figure of burr walnut: the slightest change of direction shifts the phase a lot. Now the noise is stretched along eight fixed directions, and the two nearest to the pile are mixed.
  • With a Gaussian bell the trail had soft edges, more a smudge than the mark of a hand: a flat bell makes it clean, and the ragged edges come from reading the field through a small noise displacement.
  • If the pile rises at once, with the exponential alone, the trail fades while you look at it and does not feel like velvet: every brushed cell keeps the mark for a third of RELAX and then lets go.
  • A field relaxing by itself on the GPU, in an 8 bit texture, stops half way: towards the end a frame's steps are smaller than a 255th and round to zero. The field lives in the script in Float32Arrays, and the texture is only for drawing it.
  • Combed all the way the velvet turned candy pink, a plush toy: the comb lays it at 0.56 and the sheen stays low, so Added is still bordeaux with a gleam.
  • A finger dragged to brush ended in a click and put the bag in the cart: the distance moved while pressing is counted, and past 10 pixels the click does not count.

Libraries

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.

Testing

Tested on 12 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

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.

All components