
The variable weight menu
Menus, Text, Hover, Physics
NewFree
The card of an eau de parfum in a small perfumery: a faceted glass bottle half full of amber perfume, with a gold atomizer, on a stone ledge against a fig-leaf green wall. The light follows the pointer: the bottle leans towards it on the corner of its base, the perfume inside stays level and sways when the glass moves, and every facet of the glass bends what is behind it and catches the light at its own angle. A press on the atomizer, or Try a spray, throws a cloud of droplets that curls in the air, and out of the cloud the notes of the perfume gather droplet by droplet and settle at their heights: the top note highest, the base note lowest. Then they fade in the order a perfume fades on the skin.
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 perfume card</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/playfair@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/playfair@5.3.0/standard-italic.css">
<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 card of a perfume in a small perfumery. In its picture a faceted glass bottle of amber perfume: the
perfume leans as the bottle moves, the facets bend what is behind them and catch the light, which follows
the pointer. A press on the atomizer, or Try a spray, throws a cloud of mist, and the notes of the perfume
gather out of it and settle at their heights: the top note highest, the base note lowest. Then they fade in
the order a perfume fades on the skin. The bottle, the mist and the light are drawn by the script -->
<article class="scent" aria-labelledby="scent-name" data-scent data-autoclic>
<div class="shot" data-shot>
<canvas class="shot__canvas" role="img" aria-label="A faceted glass bottle of amber perfume with a gold atomizer, standing on dark stone" data-canvas></canvas>
<div class="notes" aria-hidden="true">
<p class="note note--top" data-note><span class="note__tier">Top</span><span class="note__name" data-word>Bergamot</span></p>
<p class="note note--heart" data-note><span class="note__tier">Heart</span><span class="note__name" data-word>Fig leaf</span></p>
<p class="note note--base" data-note><span class="note__tier">Base</span><span class="note__name" data-word>Cedar</span></p>
</div>
<button class="shot__nozzle" type="button" aria-label="Spray the perfume" data-nozzle></button>
</div>
<div class="scent__body">
<p class="scent__kind">Atelier Ombrine · Eau de parfum</p>
<div class="scent__head">
<h2 class="scent__name" id="scent-name">Fig Hour</h2>
<p class="scent__price" aria-live="off"><span class="sr" data-price-sr>€115</span><span class="price" aria-hidden="true" data-price>€115</span></p>
</div>
<div class="sizes" role="radiogroup" aria-label="Size" data-sizes>
<span class="sizes__thumb" aria-hidden="true" data-thumb><span class="sizes__thumb-fill"></span></span>
<button class="size" type="button" role="radio" aria-checked="false" tabindex="-1" data-size>30 ml</button>
<button class="size" type="button" role="radio" aria-checked="true" tabindex="0" data-size>50 ml</button>
<button class="size" type="button" role="radio" aria-checked="false" tabindex="-1" data-size>100 ml</button>
</div>
<div class="buy">
<button class="pill pill--try" type="button" data-try>
<span class="pill__icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M5.2 11.2h7.6a1.4 1.4 0 0 1 1.4 1.4v6.8a1.4 1.4 0 0 1-1.4 1.4H5.2a1.4 1.4 0 0 1-1.4-1.4v-6.8a1.4 1.4 0 0 1 1.4-1.4z"/>
<path d="M7.6 11.2V8.4h2.8v2.8"/>
<path d="M6.8 8.4V5.6h4.4v2.8"/>
<g class="try__puff" data-puff>
<path d="M14.4 6.2h2.1"/>
<path d="M14.2 3.8l1.8-.9"/>
<path d="M14.2 8.6l1.8.9"/>
<path d="M18.6 6.2h1.6"/>
<path d="M18.1 2.5l1.3-.6"/>
<path d="M18.1 9.9l1.3.6"/>
</g>
</svg>
</span>
<span class="pill__label">Try a spray</span>
</button>
<button class="pill pill--add" type="button" data-add>
<span class="pill__icon" aria-hidden="true">
<svg class="add__glyph" viewBox="0 0 24 24" data-bag><path d="M5.4 8.4h13.2l-1 11.2a1.6 1.6 0 0 1-1.6 1.4H8a1.6 1.6 0 0 1-1.6-1.4z"/><path d="M9 10.6V7a3 3 0 0 1 6 0v3.6"/></svg>
<svg class="add__glyph add__drop" viewBox="0 0 24 24" data-drop><path d="M12 9.2c1.5 2 2.3 3.3 2.3 4.4a2.3 2.3 0 0 1-4.6 0c0-1.1.8-2.4 2.3-4.4z"/></svg>
<svg class="add__glyph add__tick" viewBox="0 0 24 24" data-tick><path d="M5.6 12.6l4.1 4.1 8.7-9.2"/></svg>
</span>
<span class="pill__label" data-label>Add to bag</span>
</button>
</div>
<p class="sr" aria-live="polite" data-live></p>
</div>
</article>
</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.
55 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 perfume card” from Animorio and fit it into my siteThe bottle’s cavity is an octagon and the perfume is the part of it under a line: the points p with m·p ≥ h, where m is the surface’s normal. For every angle of the surface, the height h that leaves exactly the perfume’s area underneath is found by bisection (24 steps, clipping the octagon with the line and measuring the area with the shoelace formula): so the perfume, as it tilts, rises on one side and sinks on the other without growing or shrinking. Between the two cut points the surface is bent by a cosine, which has no area and so does not change the volume: it is the wave’s second mode, the slosh.
The bottle leans towards the light on a damped spring, and turns on the corner of its base on the side it leans to (the pivot changes side when the angle goes through zero, so without a jump). The surface’s angle in the room is a lightly damped pendulum: the bottle’s acceleration pushes it off level, then it swings back past and sways. In the drawing, the surface’s angle against the glass is its angle in the room minus the bottle’s lean. The spray gives the bottle and the second mode a kick back, and so does a change of size.
Every frame, a hidden canvas gets what is behind and inside the glass: the wall and the ledge already drawn, the thick base lit with the perfume’s colour, the cavity, the tube, the perfume with its light. Then, facet by facet, the real canvas is clipped to the facet’s polygon and that canvas is copied in: the flat face magnified by 4.5% round the middle, every bevel shifted along its slope. The perfume and the edge of the ledge break at the edges, as in thick glass. Every facet has a normal in 3D: the pointer’s light gives its shade (Lambert), its glint (Blinn, with the vector half-way between the light and the eye) and, on the brightest facet, a red thread and a blue one a hair apart, as a prism does.
The spray is about nine hundred droplets in 0.22 seconds and some fifty soft puffs, with speeds and directions spread in a cone: they brake against the air (exponentially), rise slowly and are carried by a wind made from the curl of a sum of waves that drift with time. A wind like that has no sources and no sinks: the mist curls without piling up or vanishing into a point. The wind comes in gently, only once a droplet has slowed, so the jet stays straight near the nozzle. Everything is drawn in lighter mode, brighter near the light.
Every note is drawn once on a hidden canvas, letter by letter, in its own typeface and at its own place (taken from the DOM with offsetLeft and offsetTop, which do not feel GSAP’s transforms); the inked pixels become the points to reach. On a spray, up to 270 droplets set off with the mist for every word: they fly free, then a smoothstep curve carries them from their flight to their point, and the word appears made of glitter. Then the real letters condense over it: GSAP takes them from blurred, transparent and thin (wght axis at 320) to sharp and full (460), with a bounce, while the droplets go out. To fade, the opposite: the letters rise and blur and a thread of vapour leaves from the word’s points.
The chosen size is a radiogroup with the arrow keys: the pale pill under it slides with its bounce (position and width in pixels, measured again when the card changes size), the bottle grows or shrinks on an elastic and the price rolls: the old digits go up one after another and the new ones come from below. Add to bag drops a drop into the bag, which dips to catch it, then shows the tick and comes back to rest.
| Name | File | Default | What it changes |
|---|---|---|---|
TILT | script.js | 4 | How far the bottle leans towards the light, in degrees. |
SLOSH | script.js | 1 | How much the perfume sways inside when the bottle moves (0 = it stays still). |
REFRACT | script.js | 8 | How far the bevelled facets shift what is seen through them, in pixels. |
FILL | script.js | 0.64 | How full the bottle is, from 0.2 to 0.95. |
MIST | script.js | 900 | How many droplets one spray throws. |
SPRAY_SPEED | script.js | 560 | How fast the droplets leave the nozzle, in pixels a second. |
SWIRL | script.js | 1 | How much the air curls the mist (0 = it flies straight). |
HOLD | script.js | 3.2 | The seconds the notes stay before the top note starts to fade. |
DRYDOWN | script.js | 1.7 | The seconds between one note fading and the next: top, heart, then base. |
PRICES | script.js | [78, 115, 168] | The price of each size, 30, 50 and 100 ml. |
SCALES | script.js | [0.84, 1, 1.14] | How big the bottle is drawn for each size. |
CURRENCY | script.js | '€' | The currency before the price. |
WORDS | script.js | ['Adding', 'In your bag'] | Add to bag while it works and once done. |
For the card of a perfume, a candle, an oil or a diffuser in an online shop, where the notes are the thing to get across; wherever a product in glass has to look real (a liqueur, an olive oil, a serum) and a product page wants a moment to try before the bag.
The card is 340 pixels wide, or the screen less 32, and the picture is drawn again at its real size; the notes are measured on the picture’s width (cqw units), so they keep their place at any size. A tap on the picture sprays; dragging a finger sideways, the light follows the finger and the bottle leans, and letting go it goes back to its place (the page scrolls up and down as usual). The pills are 50 pixels tall and the sizes 34. Under 360 pixels the name and the pills tighten a little. The mist and the words made of droplets are the same as on a computer.
The bottle does not lean and the perfume does not sway; the light stays in its place. The spray is a still mist that comes and goes in a fade, with no flying droplets, and the notes come and go in the same order with a fade. The bottle changes size at once and the new price comes in with a fade; the buttons’ words change in a fade and the pills do not swell.
GSAP 3.15.0, Playfair (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 20 KB; the libraries: GSAP 28 KB.
Tested on 17 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.