Skip to content
animorio
IT
All components

The wax seal button

Buttons, SVG, Physics, TransitionsGSAPAdvancedCompressed weight: Demo 10 KBGSAP 28 KBFreeDownload the zip

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.

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 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)">&amp;</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 &amp; 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>

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.

21 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 wax seal button” from Animorio and fit it into my site

What it does

On a white background an ivory pill, like thick cotton paper, with a round sunken well on its left holding a line drawn nib, and the words Sign & send. With the mouse over it the button rises by a pixel and the nib tilts as if to write. Pressed, the nib goes and the words turn into Sealing; from the top left a stick of dark red sealing wax comes in, stops over the well and stirs in small circles: four drops fall, and each lands with a splash and merges with the others into a pool that spreads, wobbles a little and settles, its ragged edge drifting slowly while the wax is liquid. The stick leaves and a brass seal comes down from above, seen from overhead: its knob and collar draw in as it nears and its shadow sharpens. As it touches, the button squashes and springs back and the wax is suddenly squeezed wider into new lobes round the seal; then the seal lifts and fades, and the wax keeps a round pressed field with a raised lip round it, a thin ring and a raised ampersand. The light grazes across from one side and climbs to its place, and the relief catches it; the words turn into Sent. Moving the mouse, the light comes from its side and the shadows of the relief turn. Another click peels the seal off, it lifts and fades, and Sign & send is back.

How it works

The pool is a metaball field

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

Thick: springs and harmonics

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.

The relief, with real light

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 in perspective

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.

The sequence

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.

Values to change

NameFileDefaultWhat it changes
DROPSscript.js4How many drops of wax fall from the stick.
POOLscript.js35The radius of the pool before the seal presses, in pixels.
SPREADscript.js1.17How much wider the seal squeezes the pool.
RAGGEDscript.js0.075How ragged the edge of the pool is (0 = a perfect circle).
THICKscript.js0.42How the wax settles: low, it wobbles like jelly; near 1, it creeps like honey.
PRESSscript.js0.07How much the button gives under the seal.
LIGHTscript.js1How much the light on the relief follows the pointer (0 = it stays top left).
MARKscript.js&What the seal leaves raised in the wax: one character or two initials.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • A GSAP fromTo inside a timeline applies at once, even when it is queued for later: the light started low and grazing from the first instant and the pool looked dark for the whole drip. Tweens that come later need immediateRender: false.
  • The function that tracked the running tweens replaced their onComplete: the drops fell but never became wax. The new onComplete must call the old one too.
  • Heights cannot be made with opacity: two overlapping shapes add up their alpha and a pressed field lower than the pool becomes impossible. The grey of opaque shapes, painted one over the other, behaves like a real height map, and an feColorMatrix moves it into the alpha.
  • With the camera at the middle of the button the seal, still high up, ended far to the left of the well and seemed to arrive from outside: the camera hangs just above and to the right of the well.
  • With the opacity growing all the way down the seal stayed see-through until it touched and the wax showed through it: the opacity is full within the first 0.2 seconds.
  • The well and the words sit left and right: the stick comes in from the top left because, leaning to the right, it passed over the words.

Libraries

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.

Testing

Tested on 5 October 2026

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

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