
The star rating
Ratings, SVG, Hover
NewFree
A travel agency's Book my trip button, with a navy leather passport standing in its round end: on a click the passport opens, a wooden rubber stamp drops from above and thumps a red arrival stamp on the page, then flies off while a little plane crosses the pill and the words turn to Trip booked. Every booking leaves a new stamp, with another city.
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 stamped passport button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the stamped passport button: the booking button of a travel agency. A navy leather passport with gold foil,
stitching and an e-passport chip stands in the round left end of the pill. A click opens it: the cover swings
round its spine, a loose page flutters, a wooden rubber stamp drops from above and thumps an arrival stamp on
the visa page, then flies off while a little plane crosses the pill and the words turn to Trip booked. Every
new booking leaves another stamp, with another city; a second click closes the passport -->
<div class="pp" data-pp>
<button class="pp__btn" type="button" data-btn data-autoclic>
<span class="pp__fill" aria-hidden="true"><span class="pp__timer" data-timer></span></span>
<span class="pp__sky" aria-hidden="true">
<svg class="pp__trail" data-trail preserveAspectRatio="none"><path data-trail-path fill="none" stroke-width="1.2" stroke-linecap="round" stroke-dasharray="2 4"/></svg>
<svg class="pp__plane" data-plane viewBox="-8 -8 16 16" width="16" height="16"><path d="M7 0 C7 -0.9 6.2 -1.2 5.2 -1.2 L1.8 -1.2 L-1.6 -6.6 L-3.2 -6.6 L-1.4 -1.2 L-4.4 -1.2 L-5.6 -3 L-6.8 -3 L-6 0 L-6.8 3 L-5.6 3 L-4.4 1.2 L-1.4 1.2 L-3.2 6.6 L-1.6 6.6 L1.8 1.2 L5.2 1.2 C6.2 1.2 7 0.9 7 0 Z"/></svg>
</span>
<span class="pp__faces">
<span class="pp__face" data-face="rest">
<span class="pp__word" data-label-text>Book my trip</span>
<span class="pp__note" data-note-text>Lisbon · 3 nights</span>
</span>
<span class="pp__face pp__face--busy" data-face="busy" aria-hidden="true">
<span class="pp__shine" data-busy-text>Checking passport…</span>
</span>
<span class="pp__face pp__face--done" data-face="done" aria-hidden="true">
<svg class="pp__tick" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path data-tick d="M3 8.6 L6.6 12 L13 4.4" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="pp__done" data-done-text>Trip booked</span>
</span>
</span>
</button>
<svg class="pp__art" viewBox="0 0 112 156" width="129" height="179" aria-hidden="true" data-art>
<defs>
<linearGradient id="pp-leather" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" style="stop-color:var(--leather-light)"/>
<stop offset="0.45" style="stop-color:var(--leather)"/>
<stop offset="1" style="stop-color:var(--leather-deep)"/>
</linearGradient>
<pattern id="pp-pebble" width="2.6" height="2.6" patternUnits="userSpaceOnUse">
<circle cx="0.7" cy="0.8" r="0.42" fill="#fff" fill-opacity="0.09"/>
<circle cx="2" cy="2" r="0.36" fill="#000" fill-opacity="0.18"/>
</pattern>
<linearGradient id="pp-gold" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" style="stop-color:var(--gold-light)"/>
<stop offset="0.5" style="stop-color:var(--gold)"/>
<stop offset="1" style="stop-color:var(--gold-deep)"/>
</linearGradient>
<linearGradient id="pp-paper" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color:var(--paper-shade)"/>
<stop offset="0.14" style="stop-color:var(--paper)"/>
<stop offset="1" style="stop-color:var(--paper-light)"/>
</linearGradient>
<linearGradient id="pp-paper-left" x1="1" y1="0" x2="0" y2="0">
<stop offset="0" style="stop-color:var(--paper-shade)"/>
<stop offset="0.14" style="stop-color:var(--endpaper)"/>
<stop offset="1" style="stop-color:var(--paper-light)"/>
</linearGradient>
<linearGradient id="pp-sheen" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#fff" stop-opacity="0"/>
<stop offset="0.5" stop-color="#fff" stop-opacity="0.32"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<radialGradient id="pp-knob" cx="0.36" cy="0.3" r="0.75">
<stop offset="0" style="stop-color:var(--wood-light)"/>
<stop offset="0.6" style="stop-color:var(--wood)"/>
<stop offset="1" style="stop-color:var(--wood-deep)"/>
</radialGradient>
<linearGradient id="pp-wood" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color:var(--wood-deep)"/>
<stop offset="0.35" style="stop-color:var(--wood-light)"/>
<stop offset="0.7" style="stop-color:var(--wood)"/>
<stop offset="1" style="stop-color:var(--wood-deep)"/>
</linearGradient>
<linearGradient id="pp-metal" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f4f6f7"/>
<stop offset="0.45" stop-color="#c4ccd1"/>
<stop offset="0.55" stop-color="#8e989f"/>
<stop offset="1" stop-color="#dfe4e7"/>
</linearGradient>
<!-- rubber-stamp ink: the edges wobble a little and the ink misses the paper here and there -->
<filter id="pp-ink" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="2" seed="5" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="0.9" xChannelSelector="R" yChannelSelector="G" result="wobble"/>
<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 1.86" result="holes"/>
<feComposite in="wobble" in2="holes" operator="in"/>
</filter>
<clipPath id="pp-cover-clip"><rect x="52" y="84" width="44" height="62" rx="3"/></clipPath>
<clipPath id="pp-page-clip"><rect x="52.4" y="84.8" width="42.8" height="60.4" rx="2"/></clipPath>
</defs>
<!-- the soft shadow the passport throws on the pill -->
<ellipse data-shadow cx="74" cy="148.4" rx="25" ry="2.4" fill="#062a2c" fill-opacity="0.26"/>
<g data-book>
<!-- the back cover, peeking out behind the pages -->
<rect x="52" y="84" width="44" height="62" rx="3" fill="url(#pp-leather)"/>
<!-- the silk bookmark ribbon, hanging out between the pages -->
<g data-ribbon>
<path d="M85.6 138 L85.6 155 L87.1 153.2 L88.6 155 L88.6 138 Z" style="fill:var(--ribbon)"/>
<path d="M86.3 140 L86.3 153.4" stroke="#fff" stroke-opacity="0.35" stroke-width="0.4" stroke-linecap="round"/>
</g>
<!-- the edges of the pages, a little block of paper -->
<g fill="none" stroke-width="0.35" style="stroke:var(--paper-shade)">
<path d="M95.7 86 L95.7 144"/><path d="M95.1 85.6 L95.1 144.4"/>
</g>
<!-- the next page, seen only while the visa page lifts -->
<rect x="52.4" y="85.2" width="42.4" height="59.6" rx="2" fill="url(#pp-paper)" stroke="#d9cdb2" stroke-width="0.25"/>
<g clip-path="url(#pp-page-clip)" data-leaf-lines fill="none" stroke-width="0.25" style="stroke:var(--guilloche)" stroke-opacity="0.45"></g>
<!-- the visa page, where the stamps land: it lifts after the cover and falls back -->
<g data-page data-leaf>
<rect x="52.4" y="84.8" width="42.8" height="60.4" rx="2" fill="url(#pp-paper)"/>
<g clip-path="url(#pp-page-clip)">
<g data-guilloche fill="none" stroke-width="0.25" style="stroke:var(--guilloche)" stroke-opacity="0.75"></g>
<text class="pp__tiny" x="74" y="90.2" text-anchor="middle">VISAS</text>
<g fill="none" stroke-width="0.25" style="stroke:var(--guilloche)" stroke-opacity="0.6">
<rect x="55.6" y="92.4" width="36.4" height="24.6" rx="0.8"/>
<rect x="55.6" y="118.2" width="36.4" height="24.6" rx="0.8"/>
</g>
<g data-marks></g>
<ellipse data-stamp-shadow cx="74" cy="104.6" rx="12" ry="5" fill="#2a1a12" fill-opacity="0"/>
</g>
<!-- the gutter: the paper curves into the spine -->
<rect x="52.4" y="84.8" width="3.6" height="60.4" fill="#5b4a2e" fill-opacity="0.12"/>
<rect data-shade x="52.4" y="84.8" width="42.8" height="60.4" rx="2" fill="#0b1626" fill-opacity="0"/>
</g>
<!-- the front cover: leather outside, endpaper inside; it turns round the spine -->
<g data-cover>
<g data-cover-out>
<rect x="52" y="84" width="44" height="62" rx="3" fill="url(#pp-leather)"/>
<rect x="52" y="84" width="44" height="62" rx="3" fill="url(#pp-pebble)"/>
<path d="M53 86.6 Q53 84.6 55 84.6 L93.6 84.6" fill="none" stroke="#fff" stroke-opacity="0.18" stroke-width="0.6" stroke-linecap="round"/>
<rect x="52" y="84" width="3.2" height="62" fill="#000" fill-opacity="0.24"/>
<path d="M55.4 84 L55.4 146" stroke="#fff" stroke-opacity="0.07" stroke-width="0.5"/>
<rect x="56.6" y="86.6" width="37" height="56.8" rx="1.8" fill="none" stroke="url(#pp-gold)" stroke-opacity="0.65" stroke-width="0.42" stroke-dasharray="1.3 0.9"/>
<g data-foil>
<text class="pp__foil" x="75" y="95.6" text-anchor="middle" data-cover-title>PASSPORT</text>
<!-- the crest: a globe between two branches of laurel -->
<g fill="none" stroke="url(#pp-gold)" stroke-width="0.6">
<circle cx="75" cy="113" r="7.6"/>
<ellipse cx="75" cy="113" rx="3.6" ry="7.6"/>
<path d="M75 105.4 L75 120.6"/>
<path d="M68 109.6 Q75 111.4 82 109.6"/><path d="M67.4 113 L82.6 113"/><path d="M68 116.4 Q75 114.6 82 116.4"/>
<path d="M65 106.4 Q62.6 113.6 67.4 120.2"/><path d="M85 106.4 Q87.4 113.6 82.6 120.2"/>
</g>
<g fill="url(#pp-gold)">
<ellipse cx="64.6" cy="109.4" rx="0.9" ry="1.8" transform="rotate(-24 64.6 109.4)"/>
<ellipse cx="64" cy="113.4" rx="0.9" ry="1.8" transform="rotate(-6 64 113.4)"/>
<ellipse cx="64.8" cy="117.4" rx="0.9" ry="1.8" transform="rotate(18 64.8 117.4)"/>
<ellipse cx="85.4" cy="109.4" rx="0.9" ry="1.8" transform="rotate(24 85.4 109.4)"/>
<ellipse cx="86" cy="113.4" rx="0.9" ry="1.8" transform="rotate(6 86 113.4)"/>
<ellipse cx="85.2" cy="117.4" rx="0.9" ry="1.8" transform="rotate(-18 85.2 117.4)"/>
</g>
<text class="pp__foil-small" x="75" y="129" text-anchor="middle">TRAVEL DOCUMENT</text>
<!-- the e-passport chip mark -->
<g fill="none" stroke="url(#pp-gold)" stroke-width="0.5">
<rect x="70.6" y="133.2" width="8.8" height="5.6" rx="1.4"/>
<path d="M73.6 133.2 L73.6 138.8 M76.4 133.2 L76.4 138.8 M70.6 136 L79.4 136"/>
</g>
</g>
<!-- worn corners -->
<path d="M93.4 84.4 Q95.6 84.4 95.6 86.6" fill="none" stroke="#fff" stroke-opacity="0.3" stroke-width="0.7" stroke-linecap="round"/>
<path d="M95.6 143.4 Q95.6 145.6 93.4 145.6" fill="none" stroke="#fff" stroke-opacity="0.16" stroke-width="0.7" stroke-linecap="round"/>
<g clip-path="url(#pp-cover-clip)">
<rect data-sheen x="30" y="80" width="14" height="70" fill="url(#pp-sheen)" transform="skewX(-18)"/>
</g>
</g>
<!-- the inside of the cover, drawn mirrored: once the cover has turned it reads the right way -->
<g data-cover-in visibility="hidden">
<g transform="matrix(-1 0 0 1 148 0)">
<rect x="52" y="84" width="44" height="62" rx="3" fill="url(#pp-leather)"/>
<rect x="52.4" y="84.8" width="42.8" height="60.4" rx="2" fill="url(#pp-paper-left)"/>
<g data-rosette fill="none" stroke-width="0.22" style="stroke:var(--guilloche)" stroke-opacity="0.7"></g>
<text class="pp__tiny" x="74" y="90.2" text-anchor="middle">TRAVEL DOCUMENT</text>
<g style="fill:var(--ink-print)" fill-opacity="0.5">
<rect x="57" y="114" width="16" height="1" rx="0.5"/><rect x="57" y="117" width="22" height="1" rx="0.5"/>
<rect x="57" y="120" width="12" height="1" rx="0.5"/><rect x="76" y="114" width="14" height="1" rx="0.5"/>
</g>
<!-- the holder's signature on its line -->
<path d="M57 127.6 L91 127.6" stroke-width="0.25" style="stroke:var(--ink-print)" stroke-opacity="0.5"/>
<path d="M58.6 126.6 C60 122.6 61.4 122.4 61.2 125 C61 127 62.6 124 64 123.8 C65.6 123.6 64.4 126.4 66 126 C67.6 125.6 68.6 123.4 70.4 124.2 C72 125 71 126.6 73.4 125.8 C75.4 125.2 77 124.4 79 125.2" fill="none" stroke-width="0.4" style="stroke:var(--ink-pen)" stroke-linecap="round"/>
<!-- the machine-readable lines at the bottom -->
<text class="pp__mrz" x="56.4" y="137.4" data-mrz-1>P<WYF<TRAVELLER<<<<<</text>
<text class="pp__mrz" x="56.4" y="141.6" data-mrz-2>X4821905<7WYF9605<<<</text>
</g>
</g>
</g>
</g>
<!-- the rubber stamp: a wooden knob, a brass collar, the block with its city label and the red rubber -->
<g data-stamp opacity="0">
<g data-stamp-tilt>
<g data-stamp-body>
<ellipse cx="0" cy="-37" rx="6.6" ry="5.8" fill="url(#pp-knob)"/>
<ellipse cx="-2.2" cy="-39.2" rx="2" ry="1.3" fill="#fff" fill-opacity="0.35"/>
<path d="M-2.6 -31.6 L2.6 -31.6 L3.6 -19 L-3.6 -19 Z" fill="url(#pp-wood)"/>
<path d="M-1.2 -30 L-1.6 -20.4" stroke="#fff" stroke-opacity="0.25" stroke-width="0.6" stroke-linecap="round"/>
<rect x="-5.6" y="-20.2" width="11.2" height="2.8" rx="0.9" fill="url(#pp-metal)"/>
<rect x="-11" y="-17.6" width="22" height="11.4" rx="1.6" fill="url(#pp-wood)"/>
<g fill="none" stroke="#3a2210" stroke-opacity="0.28" stroke-width="0.35" stroke-linecap="round">
<path d="M-10 -15.4 Q-3 -14.6 4 -15.6 T10 -15"/><path d="M-10 -9 Q-2 -8.2 3 -9.4 T10 -8.6"/>
</g>
<rect x="-6.4" y="-15" width="12.8" height="5.6" rx="0.6" fill="#fbf4e4"/>
<text class="pp__stamp-label" x="0" y="-10.9" text-anchor="middle" data-stamp-label>LIS</text>
<rect x="-11.6" y="-6.4" width="23.2" height="2.6" rx="0.6" fill="url(#pp-metal)"/>
<rect x="-10.6" y="-3.8" width="21.2" height="3.8" rx="0.8" style="fill:var(--rubber)"/>
<rect x="-10.6" y="-1" width="21.2" height="1" rx="0.5" style="fill:var(--ink)" fill-opacity="0.85"/>
</g>
</g>
</g>
</svg>
<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.
44 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 stamped passport button” from Animorio and fit it into my siteA single SVG over the pill's left end that takes no pointer: the click goes to the real <button> underneath. Inside, in layers: the back cover, the edge of the pages, the visa page, a loose page and the front cover. The leather is a gradient with a pattern of light and dark dots on top for the grain; the foil and the stitching use a gold gradient.
A number from 0 to 1 becomes scaleX = cos(πp) and skewY = -7·sin(πp) around the spine (svgOrigin): the cover narrows, passes edge-on and opens out the other side with a touch of perspective. When scaleX turns negative the leather face goes off and the inside face comes on, drawn already mirrored with a matrix, so after the turn it reads the right way. A shadow on the page follows sin(πp).
The stamp drops in with back.out, winds up and strikes with power3.in in about a tenth of a second; on contact three there-and-back tweens squash the stamp, the passport (around its feet) and the pill. The stamp's shadow on the page widens and darkens as it comes closer.
The visa is SVG text and rectangles with a filter: feTurbulence makes the noise, feDisplacementMap barely moves the edges, and the same noise, passed through a feColorMatrix, punches holes in the ink where the rubber missed. On impact it is full and glossy, then over a second and a half it turns matt; six specks around it pop out with back.out.
An SVG plane follows an arc over the words: on every frame its position and angle come from the curve, and the dots of the trail, already placed along the arc, light up as the plane passes them.
| Name | File | Default | What it changes |
|---|---|---|---|
LABEL | script.js | Book my trip | The words at rest. |
NOTE | script.js | Lisbon · 3 nights | The small words under them. |
BUSY | script.js | Checking passport… | The words while the passport is checked. |
DONE | script.js | Trip booked | The words once it is stamped. |
CITIES | script.js | LIS,KYO,OSL,CUZ,MRK | The city codes of the stamps, one per booking, in turn. |
DATE | script.js | 24 OCT 2026 | The date printed under the city. |
THUMP | script.js | 1 | How hard the stamp comes down: 0.4 gentle, 1.6 heavy. |
SPEED | script.js | 1 | How fast the whole thing runs: 0.6 slow, 1.6 quick. |
For the booking button of a travel agency, an airline, a hotel or a travel app, or for a button that confirms a document, a permit or a registration.
The component is 330 pixels wide or the screen minus 32, and 156 tall with a 60 pixel pill; at rest the small words sit under the big ones. With no hover, the finger resting on it opens the cover a crack like the mouse.
No hops, no turning cover, no stamp coming down and no plane: on a click the passport shows itself open in a fade, the visa fades in and the words change by fading.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.
Tested on 23 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.