Skip to content
animorio
IT
All components

The perfume card

Cards, Physics, Hover, TextCanvas, GSAP, CSS onlyAdvancedCompressed weight: Demo 20 KBGSAP 28 KBFreeDownload the zip

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.

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

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.

55 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 perfume card” from Animorio and fit it into my site

What it does

On a white page, a white card with round corners. At the top the picture, drawn by the script: a dark green wall with a warm light brightening it at the top left, a stone ledge and on it a perfume bottle of thick glass, cut as an octagon, with bevelled facets all round the flat face in front. Inside, the amber perfume to a little over half, darker at the bottom, the atomizer’s tube going down to the bottom and a paper label with the name and the size; on top the neck, the ribbed gold collar and the atomizer’s head, with its little hole on the right. Under the bottle its shadow and, inside the shadow, a patch of amber light: the light that came through the perfume. With the pointer over the picture the light goes where the pointer goes: the bright patch on the wall follows it, the shadow swings the other way, the bottle leans a few degrees towards the light, rising on the corner of its base, and the perfume stays level, sways, sloshes and settles. Every bevelled facet shows the perfume and the edge of the ledge shifted, as thick glass does, and the one turned to the light lights up with a glint and a thread of colours, red on one side and blue on the other. Pressing the atomizer’s head (or Try a spray, or a tap on the picture) the head goes down, the bottle kicks back and the perfume sloshes; out of the little hole comes a jet of very fine droplets that widens, slows, curls and leaves a light mist. Out of the mist some of the droplets fly off to draw three words: the droplets make the letters and the letters condense, from blurred and thin to sharp and full, rising a few pixels. They are the notes: Top Bergamot at the top, Heart Fig leaf in the middle, Base Cedar at the bottom. After a few seconds the top note evaporates, letter by letter, in a thread of droplets that rises; then the heart note, and last of all the base. Under the picture the house, the name Fig Hour, the price, the sizes 30, 50 and 100 ml (choosing one, the bottle grows or shrinks with a bounce and the price rolls digit by digit), and the buttons Try a spray and Add to bag: an amber drop falls into the bag, then In your bag with a tick, and after a few seconds it comes back as it was.

How it works

The perfume keeps its volume

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

A pendulum inside a spring

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.

The glass, drawn twice

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 mist in a curl wind

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.

Words made of droplets

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 price and the sizes

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.

Values to change

NameFileDefaultWhat it changes
TILTscript.js4How far the bottle leans towards the light, in degrees.
SLOSHscript.js1How much the perfume sways inside when the bottle moves (0 = it stays still).
REFRACTscript.js8How far the bevelled facets shift what is seen through them, in pixels.
FILLscript.js0.64How full the bottle is, from 0.2 to 0.95.
MISTscript.js900How many droplets one spray throws.
SPRAY_SPEEDscript.js560How fast the droplets leave the nozzle, in pixels a second.
SWIRLscript.js1How much the air curls the mist (0 = it flies straight).
HOLDscript.js3.2The seconds the notes stay before the top note starts to fade.
DRYDOWNscript.js1.7The seconds between one note fading and the next: top, heart, then base.
PRICESscript.js[78, 115, 168]The price of each size, 30, 50 and 100 ml.
SCALESscript.js[0.84, 1, 1.14]How big the bottle is drawn for each size.
CURRENCYscript.js'€'The currency before the price.
WORDSscript.js['Adding', 'In your bag']Add to bag while it works and once done.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • In the first version the mist all stopped at the same distance from the nozzle and made a white heap like sand over the first note. The droplets now leave at very different speeds (from 25% to 120%), die sooner, the curl wind is stronger once they have slowed and the puffs are more transparent: the jet opens into a mist and does not cover the words.
  • The droplets bound for the letters, bright from the start, made a clump of glitter before they even set off for the words: now they are almost dark while they fly and light up only as they arrive (their brightness follows the square of their progress), and the real letters condense only once the word of droplets can already be read.
  • The glint worked out by mirroring the light in the facet (Phong) almost never showed: with the light in front of the wall, the mirrored ray almost never reached the eye. With the vector half-way between the light and the eye (Blinn) the bevel turned to the light really lights up.
  • The bottle, leaning round the middle of its base, sank one corner into the stone. Now it turns on the corner on the side it leans to, and the other one rises: the pivot changes side when the angle goes through zero, where the two turns are the same, so it does not jump.
  • The first change of price did not roll: the price in the HTML was one piece of text and there were no digits to send up. At the start the script writes it again from PRICES, one digit to a span.

Libraries

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.

Testing

Tested on 17 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