
The cookie banner
Consent, Transitions, SVG, Physics
NewFree
The Sign & send of a contract, closed like a letter: pressed, a stick of dark red sealing wax drips into the button’s well and the wax spreads, thick and slow, into a pool with a ragged edge. A brass seal comes down from above in perspective, presses (the button gives and the wax is squeezed wider) and lifts: an ampersand stays raised in the wax, lit by real SVG lighting filters, with the light following the pointer. The words turn into Sent.
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 wax seal button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/opsz.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/opsz-italic.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the wax seal button: pressed, a stick of sealing wax drips into the round well and stirs a pool with a ragged
edge, a brass seal comes down from above, presses, lifts, and leaves an ampersand raised in the wax that
catches the light. The pool and the relief are an SVG path redrawn every frame under a lighting filter -->
<div class="seal" data-seal>
<button class="seal__btn" type="button" aria-describedby="seal-live" data-btn data-autoclic>
<span class="seal__well" aria-hidden="true" data-well>
<svg class="seal__nib" viewBox="0 0 24 24" data-nib>
<path d="M12 2.8l5.2 7.6c.3.5.3 1.1 0 1.6L12 21.2 6.8 12c-.3-.5-.3-1.1 0-1.6z"/>
<path d="M12 21.2v-7"/>
<circle cx="12" cy="12.4" r="1.6"/>
</svg>
</span>
<!-- the pool of wax, its shadow and the relief: drawn round the middle of the well -->
<svg class="seal__wax" viewBox="-100 -100 200 200" aria-hidden="true" data-wax>
<defs>
<filter id="wax-relief" filterUnits="userSpaceOnUse" x="-100" y="-100" width="200" height="200" color-interpolation-filters="sRGB">
<!-- the grey of each shape is its height: the pool, the lip pushed up round the die, the pressed field,
the ring and the ampersand -->
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.333 0.333 0.334 0 0" result="steps"/>
<feGaussianBlur in="steps" stdDeviation="0.9" result="fine"/>
<!-- and the whole pool is a soft dome at its edge -->
<feGaussianBlur in="SourceAlpha" stdDeviation="3.4" result="dome"/>
<feComposite in="fine" in2="dome" operator="arithmetic" k2="0.62" k3="0.55" result="height"/>
<feDiffuseLighting in="height" surfaceScale="5.5" diffuseConstant="1.18" lighting-color="#ffffff" result="diffuse">
<feDistantLight azimuth="225" elevation="46" data-light/>
</feDiffuseLighting>
<feSpecularLighting in="height" surfaceScale="5.5" specularConstant="0.8" specularExponent="26" lighting-color="#ffe7d2" result="gloss">
<feDistantLight azimuth="225" elevation="46" data-light/>
</feSpecularLighting>
<feFlood flood-color="#8e1a22" result="tint" data-tint/>
<feComposite in="tint" in2="diffuse" operator="arithmetic" k1="1.02" k2="0.16" result="lit"/>
<feComposite in="lit" in2="SourceAlpha" operator="in" result="body"/>
<feComposite in="gloss" in2="SourceAlpha" operator="in" result="shine"/>
<feComposite in="shine" in2="body" operator="arithmetic" k2="0.75" k3="1"/>
</filter>
<filter id="wax-shadow" filterUnits="userSpaceOnUse" x="-100" y="-100" width="200" height="200">
<feGaussianBlur stdDeviation="2.4"/>
</filter>
</defs>
<path class="seal__shadow" transform="translate(1.5 3.5)" data-shadow d=""/>
<g filter="url(#wax-relief)" data-relief>
<path data-pool fill="rgb(150,150,150)" d=""/>
<g data-print opacity="0" transform="scale(1.1)">
<circle r="27" fill="none" stroke="rgb(196,196,196)" stroke-width="4.6"/>
<circle r="25" fill="rgb(104,104,104)"/>
<circle r="21.4" fill="none" stroke="rgb(150,150,150)" stroke-width="1.3"/>
<text class="seal__mono" y="10.6" text-anchor="middle" fill="rgb(176,176,176)">&</text>
</g>
</g>
<g data-drops></g>
</svg>
<span class="seal__words" aria-hidden="true">
<span class="seal__word is-on" data-word="rest">Sign & send</span>
<span class="seal__word" data-word="busy">Sealing</span>
<span class="seal__word" data-word="done">Sent</span>
</span>
<span class="sr" data-name>Sign and send the contract</span>
</button>
<!-- the stick of sealing wax, and the brass seal seen from above, drawn in perspective every frame -->
<div class="seal__stick" aria-hidden="true" data-stick>
<svg viewBox="-12 -150 24 156">
<defs>
<linearGradient id="wax-stick" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#4c070d"/>
<stop offset="0.3" stop-color="#a1222b"/>
<stop offset="0.48" stop-color="#d04a4d"/>
<stop offset="0.62" stop-color="#9a1d26"/>
<stop offset="1" stop-color="#3d050a"/>
</linearGradient>
<linearGradient id="wax-tip" x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stop-color="#9a1d26"/>
<stop offset="1" stop-color="#6a0d15"/>
</linearGradient>
</defs>
<rect x="-8" y="-148" width="16" height="136" rx="3" fill="url(#wax-stick)"/>
<path d="M-8 -14c0 6 3 12 8 14 5-2 8-8 8-14z" fill="url(#wax-tip)"/>
<path d="M-4.5 -140v120" stroke="#ffd0c4" stroke-opacity="0.35" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div>
<svg class="seal__stamp" aria-hidden="true" data-stamp>
<defs>
<radialGradient id="brass-top" cx="0.36" cy="0.32" r="0.8">
<stop offset="0" stop-color="#fff3cf"/>
<stop offset="0.25" stop-color="#e9c779"/>
<stop offset="0.7" stop-color="#a77a2c"/>
<stop offset="1" stop-color="#5e3f12"/>
</radialGradient>
<linearGradient id="brass-side" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#d2a656"/>
<stop offset="0.55" stop-color="#7c561b"/>
<stop offset="1" stop-color="#3f2a0b"/>
</linearGradient>
<radialGradient id="ebony" cx="0.38" cy="0.34" r="0.8">
<stop offset="0" stop-color="#6c5a50"/>
<stop offset="0.5" stop-color="#2a1f1b"/>
<stop offset="1" stop-color="#0d0908"/>
</radialGradient>
<filter id="stamp-blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" data-blur/>
</filter>
</defs>
<ellipse class="seal__stamp-shadow" filter="url(#stamp-blur)" data-cast/>
<g data-parts></g>
</svg>
<p class="sr" id="seal-live" 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.
21 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 wax seal button” from Animorio and fit it into my siteEach drop is a ball of a field: at a point the field is the sum of r² / d² over all the drops, and the edge of the pool is where it equals 1. Far from the drops the areas add up: four drops of radius r make a pool of radius 2r, so each one has radius POOL / √DROPS. Every frame the edge is found again along 64 rays from the middle: stepping in from outside by 3 pixels until the field reaches 1, then nine bisections. The 64 points become a closed Catmull-Rom curve written as cubic Béziers in the path’s d.
Each drop lands small and fast and its radius moves towards its size on a damped spring (stiffness 150, damping THICK): the wax spreads, overshoots a little and comes back, heavy. The edge is ragged by six harmonics (3, 4, 5, 7, 9 and 12 lobes) of random sizes and phases, weighted by RAGGED; while the wax is liquid the phases drift slowly and the edge moves, when the seal presses half of the harmonics change (new squeezed lobes) and the liquid falls to zero in 1.6 seconds: the shape sets.
Inside the filtered group the grey of each shape is a height: the pool, the lip pushed up round the seal, the lower pressed field, the ring and the ampersand. An feColorMatrix moves the brightness into the alpha, a light feGaussianBlur softens it and a second, wide blur of the alpha makes the dome of the edge; the two added by an arithmetic feComposite are the height map. feDiffuseLighting and feSpecularLighting with a distant light light it, the colour of the wax is multiplied by the diffuse light (plus a little ambient) and the gloss is added on top, all cut to the shape.
The seal seen from above is a stack of discs at their heights: the brass die, the collar, the ebony neck and the knob. Each disc is projected from a camera hanging just above and to the right of the well, k = 900 / (900 − z): the higher it is the larger and the further out, so the cylinder shows its side and reads in 3D. As it comes down the discs draw in and the blurred shadow on the button closes in, sharper and darker.
A GSAP timeline brings the stick in, stirs it in circles while it lets the drops fall (each drop is an ellipse falling with an acceleration that, as it lands, becomes a ball of the field), takes it away, brings the seal down and on contact squashes the button (scaleY with an elastic return) and widens the balls by SPREAD. As it lifts the print appears and the light grazes in from −80 degrees and 24 of elevation to its place. The drawing loop sleeps when nothing moves.
| Name | File | Default | What it changes |
|---|---|---|---|
DROPS | script.js | 4 | How many drops of wax fall from the stick. |
POOL | script.js | 35 | The radius of the pool before the seal presses, in pixels. |
SPREAD | script.js | 1.17 | How much wider the seal squeezes the pool. |
RAGGED | script.js | 0.075 | How ragged the edge of the pool is (0 = a perfect circle). |
THICK | script.js | 0.42 | How the wax settles: low, it wobbles like jelly; near 1, it creeps like honey. |
PRESS | script.js | 0.07 | How much the button gives under the seal. |
LIGHT | script.js | 1 | How much the light on the relief follows the pointer (0 = it stays top left). |
MARK | script.js | & | What the seal leaves raised in the wax: one character or two initials. |
For the action that closes something important: signing and sending a contract, confirming a wedding invitation, posting a letter, accepting an offer. It suits an e-signature product, a wedding or law firm site, a hand written newsletter. The seal takes a few seconds: it is not for an action repeated often.
The button keeps its size (about 290 pixels at 390) with less room at the sides; the stick and the seal come in from above and from the left, never over the words, and nothing spills over. A tap starts the sequence; the light on the relief, with no pointer, stays top left.
No stick, no drops, no seal coming down and no squashing button: the nib fades and the seal, already pressed, fades into the well, with the words changing without sliding. The light does not follow the pointer. Another click fades it away.
GSAP 3.15.0, Fraunces (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 10 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.