Skip to content
animorio
IT
Back to the library

The praline box to open

3D, HoverCSS only, GSAPAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

The header of a made-up chocolatier, in the rich style: a box of nine pralines built in 3D, petrol green with gold foil stamping and a pink satin ribbon tied in a bow, tilting towards the pointer. A click unties the ribbon and swings the lid open on its hinge; the praline under the pointer, or the finger, lifts out of its paper cup turning, with a puff of cocoa, and its name appears on the card.

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 praline box to open</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.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="shop" data-shop>
    <!-- the header of a made-up chocolatier, Maison Noix: a box of nine pralines in 3D that tilts with the pointer.
         Click the box: the satin ribbon unties, the lid swings open on its hinge and the pralines show in their paper
         cups. The praline under the pointer (or the finger) lifts out of its cup and its name appears on the card -->
    <header class="shop__bar">
      <span class="shop__brand" data-brand>Maison Noix</span>
      <span class="shop__where">Chocolatier since 1931</span>
    </header>

    <section class="shop__copy">
      <p class="shop__kicker">The Saturday box</p>
      <h1 class="shop__title">Nine little <em>reasons.</em></h1>
      <div class="shop__card" data-card aria-live="polite">
        <span class="shop__card-name" data-name>Open the box</span>
        <span class="shop__card-note" data-note>Click or tap the lid</span>
      </div>
      <button class="shop__button" type="button" data-order><span>Order the box</span></button>
    </section>

    <div class="stage" data-stage tabindex="0" role="button"
      aria-label="A box of nine pralines. Click, tap or press Enter to open or close it; move over a praline, or use the arrow keys, to lift it and read its name.">
      <div class="stage__shadow" data-shadow></div>
      <div class="tilt" data-tilt>
        <div class="spin" data-spin>

          <!-- on the table, around the box -->
          <div class="table-bits" aria-hidden="true">
            <div class="crumb crumb--a"></div>
            <div class="crumb crumb--b"></div>
            <div class="crumb crumb--c"></div>
            <div class="nib nib--a"></div>
            <div class="nib nib--b"></div>
            <div class="menu-card"><span>Nine pieces</span><i>made by hand this morning</i></div>
          </div>

          <!-- the box: base, walls with an outer and an inner side -->
          <div class="box" aria-hidden="true">
            <div class="box__floor"></div>
            <div class="wall wall--front"><div class="wall__out"><span class="wall__label">Assortiment</span></div><div class="wall__in"></div></div>
            <div class="wall wall--back"><div class="wall__out"></div><div class="wall__in"></div></div>
            <div class="wall wall--left"><div class="wall__out"></div><div class="wall__in"></div></div>
            <div class="wall wall--right"><div class="wall__out"></div><div class="wall__in"></div></div>

            <div class="tray-pos">
              <div class="tray">
                <div class="tissue tissue--l"></div>
                <div class="tissue tissue--r"></div>
                <div class="tray__grid" data-grid></div>
              </div>
            </div>

            <div class="lid-pos">
              <div class="lid" data-lid>
                <div class="lid__under"><span>Handmade in small batches</span></div>
                <div class="lid__top">
                  <div class="foil" data-foil>
                    <svg viewBox="0 0 120 120" class="foil__mark">
                      <circle cx="60" cy="60" r="54" fill="none" stroke="currentColor" stroke-width="2"/>
                      <circle cx="60" cy="60" r="48" fill="none" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 3"/>
                      <path d="M60 30c-9 0-16 7-16 18 0 13 10 22 16 30 6-8 16-17 16-30 0-11-7-18-16-18z" fill="none" stroke="currentColor" stroke-width="2"/>
                      <path d="M60 34v40M52 46c3 3 13 3 16 0M50 56c4 3 16 3 20 0" fill="none" stroke="currentColor" stroke-width="1.4"/>
                    </svg>
                    <span class="foil__name" data-foil-name>Maison Noix</span>
                    <span class="foil__line">Pralines · Nine pieces</span>
                  </div>
                  <div class="ribbon ribbon--h" data-rib-h></div>
                  <div class="ribbon ribbon--v" data-rib-v></div>
                </div>
                <div class="skirt skirt--front"><div class="skirt__out"><div class="ribbon-drop"></div></div><div class="skirt__in"></div></div>
                <div class="skirt skirt--back"><div class="skirt__out"><div class="ribbon-drop"></div></div><div class="skirt__in"></div></div>
                <div class="skirt skirt--left"><div class="skirt__out"><div class="ribbon-drop ribbon-drop--side"></div></div><div class="skirt__in"></div></div>
                <div class="skirt skirt--right"><div class="skirt__out"><div class="ribbon-drop ribbon-drop--side"></div></div><div class="skirt__in"></div></div>
                <div class="bow" data-bow>
                  <svg viewBox="0 0 160 110" class="bow__svg">
                    <defs>
                      <linearGradient id="satin" x1="0" y1="0" x2="1" y2="1">
                        <stop offset="0" stop-color="var(--ribbon-light)"/>
                        <stop offset="0.45" stop-color="var(--ribbon)"/>
                        <stop offset="1" stop-color="var(--ribbon-dark)"/>
                      </linearGradient>
                    </defs>
                    <path class="bow__tail" d="M74 60 C66 78 58 92 44 106 L58 104 L64 110 C72 94 78 78 80 62z" fill="url(#satin)"/>
                    <path class="bow__tail" d="M86 60 C94 78 104 92 118 104 L104 104 L98 110 C90 94 84 78 80 62z" fill="url(#satin)"/>
                    <path d="M80 52 C60 22 26 14 14 30 C4 44 22 66 50 64 C62 63 72 60 80 56z" fill="url(#satin)"/>
                    <path d="M80 52 C100 22 134 14 146 30 C156 44 138 66 110 64 C98 63 88 60 80 56z" fill="url(#satin)"/>
                    <path d="M22 34 C34 26 58 30 74 50" fill="none" stroke="var(--ribbon-dark)" stroke-width="2" opacity="0.5"/>
                    <path d="M138 34 C126 26 102 30 86 50" fill="none" stroke="var(--ribbon-dark)" stroke-width="2" opacity="0.5"/>
                    <path d="M28 40 C40 34 54 38 66 50" fill="none" stroke="#fff" stroke-width="2.4" opacity="0.45" stroke-linecap="round"/>
                    <path d="M132 40 C120 34 106 38 94 50" fill="none" stroke="#fff" stroke-width="2.4" opacity="0.35" stroke-linecap="round"/>
                    <rect x="70" y="44" width="20" height="20" rx="7" fill="url(#satin)"/>
                    <path d="M74 48 C78 46 84 46 86 48" fill="none" stroke="#fff" stroke-width="1.6" opacity="0.5" stroke-linecap="round"/>
                  </svg>
                </div>
              </div>
            </div>
          </div>

        </div>
      </div>
    </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.

63 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 praline box to open” from Animorio and fit it into my site

What it does

Maison Noix is a chocolatier. To the right of the headline «Nine little reasons.» stands its Saturday box, on a blush pink table with a menu card, two crumbs of pistachio and a few cocoa nibs. The box is built in 3D with a floor, four walls that have a petrol green outer side and a cream paper inner side, and a lid whose skirts come down over the walls; on the lid the brand is gold foil stamped, shining where the light falls, and a pink satin ribbon crosses the lid, runs down the edges and is tied in a bow with its tails. Moving the pointer tilts the box on a spring, and the reflections on the lid, the gold and the pralines move the other way; left alone, it breathes gently. A click (or a tap, or Enter) unties the ribbon: the bow jumps up and vanishes, the two strips slide away; then the lid swings open on its back hinge, goes a little too far and settles, and inside are the tissue paper and the nine pralines in their pleated paper cups, which give a small hop one after the other. Every praline has its own rounded body and its own decoration: salted caramel with gold threads and salt flakes, pink raspberry with a white swirl, pistachio, hazelnut, the grand cru with gold leaf, the coffee bean, coconut, the rose petal and candied orange peel. Moving over a praline (or tapping it) lifts it out of its cup turning a little, with a puff of cocoa, and the card under the headline gives its name and notes. Another click closes the lid, which lands with a small knock, and the ribbon ties itself again with the bow dropping in with a spring.

How it works

A box made of planes

Everything sits in a container with transform-style: preserve-3d, tilted with rotateX by one element and turned with rotateZ by its child (in that order, or the box turns around the screen instead of around itself). The walls are divs rotated 90 degrees around their edge; each has two faces 2 pixels apart, green outside and cream inside, so the right one shows from every side. The sizes are CSS variables worked out on resize, so the box redraws even halfway through an animation.

The lid on its hinge

The lid is a group raised to the height of the walls, with transform-origin on its back edge: opening it is a rotateX up to 112 degrees, with a small overshoot to 116 and a swing back to 108 before it stops. On top is the outer plane with the print and the ribbon, underneath the inner plane turned 180 degrees; both have backface-visibility: hidden, so only the one facing the viewer shows.

Pralines with a body

Every praline is a stack of six round slices, narrower towards the top, moved with translateZ: tilting the box shows the rounded side. On top is the face with its SVG decoration, drawn with a fixed-seed random generator so it is the same at every visit, and a shine that moves against the pointer.

Lifting and puffing

The spot under the pointer is found with elementFromPoint, which respects 3D transforms. The praline rises with a tween of z and rotationZ with back.out, the shadow in the cup widens and fades, and seven grains of cocoa leave the centre in a circle on the tray’s plane and rise as they fade.

Values to change

NameFileDefaultWhat it changes
PRALINESscript.jsnove pralineThe nine pralines, in reading order: name, notes on the card and decoration (drizzle, swirl, nuts, hazelnut, gold, bean, dust, petal, orange).
BRANDscript.jsMaison NoixThe name in the bar and in gold on the lid.
TILTscript.js1How much the box tilts towards the pointer: 0 not at all, 2 a lot.
OPEN_TIMEscript.js0.9The seconds the lid takes to swing open.
LIFTscript.js1How high a praline rises out of its cup.
SPINscript.js1How much a lifted praline turns.
PAUSEscript.js1.4The seconds between two moves when the box plays by itself.
--box, --gold, --ribbon, --dark, --milk…style.cssverde petrolio, oro, rosaThe colours in :root: the box, the gold, the ribbon and the three chocolates of the pralines.

Try them in the Playground

When to use it

For the header of a chocolatier, a patisserie, a gift or sweet shop, the page of a gift product or a subscription box. It is a showcase: prices and the buy button also need to be in plain text.

On phones

Same box and same animations, adapted sizes: the headline and the card on top, the box below, as large as the space left. A tap opens and closes it, a tap on a praline lifts it, and dragging a finger tilts the box; the stage has touch-action none.

With reduced motion

No motion: the box stands still and already open, without the ribbon; the chosen praline stands out because the others fade, and the card changes with a fade.

Pitfalls

  • The order of rotations matters: GSAP writes rotate before rotateX, so the tilt and the turn live on two nested elements.
  • An element with 3D children (preserve-3d) and backface-visibility: hidden breaks up badly in Chrome: the lid’s plane must stay flat, with the ribbon inside it.
  • The bow must be lifted a little above the lid’s plane (z), or it ends up under it.
  • GSAP overwrites the CSS transform: the position of the groups (the height of the lid, of the tray) lives on a parent, the animated rotation on the child.

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.

Testing

Tested on 23 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 animations