Skip to content
animorio
IT
All components

The gift wrap button

Buttons, 3D, SVGCanvas, GSAPAdvancedCompressed weight: Demo 15 KBGSAP 28 KBFreeDownload the zip

The Add gift wrap · €4 line of a cart, with a small kraft box in the round end of the pill, drawn in 3D on a canvas. A click wraps it for real: a sheet of striped paper arrives open round the box, the two ends rise and slap onto the sides, then the front and the back, and the two halves of the top fold over each other; the print runs on from flap to flap and the plain back of the paper shows until a flap turns past the eye. Then a satin ribbon runs round it both ways and a bow is tied on top, an SVG drawn again every frame: the loops puff out from the knot, the tails drop and swing on a spring, a gleam runs over the satin. The pill says Gift wrapped with a small Remove pill; another click pulls a tail, unties the bow, slides the ribbon off and opens the paper.

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 gift wrap 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 gift wrap button: the Add gift wrap line of a shopping cart, with a small kraft box in the round left end
         of the pill. A click wraps it for real, in 3D: a sheet of printed paper folds up onto its faces and over its
         top, a satin ribbon runs round it both ways and a bow is tied on top, its loops puffing out and its tails
         swinging. Another click unties the bow, slides the ribbon off and opens the paper -->
    <div class="wrap" data-gift>
      <button class="wrap__btn" type="button" data-btn data-autoclic>
        <span class="wrap__fill" aria-hidden="true"></span>
        <span class="wrap__art" aria-hidden="true">
          <canvas class="wrap__box" data-box></canvas>
          <svg class="wrap__bow" viewBox="0 0 76 60" data-bow>
            <defs>
              <linearGradient id="gift-loop" x1="0" y1="0" x2="1" y2="0.35">
                <stop offset="0" class="st-dark"/>
                <stop offset="0.22" class="st-mid"/>
                <stop offset="0.5" class="st-light"/>
                <stop offset="0.68" class="st-mid"/>
                <stop offset="1" class="st-dark"/>
              </linearGradient>
              <linearGradient id="gift-tail" x1="0" y1="0" x2="0.4" y2="1">
                <stop offset="0" class="st-dark"/>
                <stop offset="0.3" class="st-mid"/>
                <stop offset="0.62" class="st-light"/>
                <stop offset="1" class="st-mid"/>
              </linearGradient>
              <linearGradient id="gift-knot" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0" class="st-light"/>
                <stop offset="0.55" class="st-mid"/>
                <stop offset="1" class="st-dark"/>
              </linearGradient>
              <linearGradient id="gift-sheen" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="10" y2="6" data-sheen>
                <stop offset="0" stop-color="#fff" stop-opacity="0"/>
                <stop offset="0.5" stop-color="#fff" stop-opacity="0.7"/>
                <stop offset="1" stop-color="#fff" stop-opacity="0"/>
              </linearGradient>
            </defs>
            <g data-bow-all>
              <path class="wrap__tail" fill="url(#gift-tail)" data-tail="l"/>
              <path class="wrap__tail" fill="url(#gift-tail)" data-tail="r"/>
              <path fill="url(#gift-loop)" data-loop="l"/>
              <path class="wrap__hole" data-hole="l"/>
              <path class="wrap__gleam" data-gleam="l"/>
              <path fill="url(#gift-loop)" data-loop="r"/>
              <path class="wrap__hole" data-hole="r"/>
              <path class="wrap__gleam" data-gleam="r"/>
              <path fill="url(#gift-knot)" data-knot/>
              <path class="wrap__crease" data-crease/>
              <path fill="url(#gift-sheen)" data-shine/>
            </g>
          </svg>
        </span>
        <span class="wrap__faces">
          <span class="wrap__face" data-face="rest">
            <span class="wrap__word" data-wide="Add gift wrap" data-rest-text>Add gift wrap</span>
            <span class="wrap__price" data-price-text>· €4</span>
          </span>
          <span class="wrap__face wrap__face--busy" data-face="busy" aria-hidden="true">
            <span class="wrap__shine" data-busy-text>Wrapping…</span>
          </span>
          <span class="wrap__face wrap__face--done" data-face="done" aria-hidden="true">
            <span class="wrap__done" data-done-text>Gift wrapped</span>
            <span class="wrap__chip" data-remove-text>Remove</span>
          </span>
        </span>
      </button>
      <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>

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.

26 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 gift wrap button” from Animorio and fit it into my site

What it does

On a white background a dark pine pill 60 pixels tall, with a thread of light along the top and a soft shadow under it. In the round left end, on a small pool of light, a little kraft box seen from above and three quarters: the base and the slightly wider lid, each face lit by the lamp at the upper left, with a fine edge and the lid's shadow on the base. On the right, in ivory Mona Sans, Add gift wrap and, dimmer, · €4. On arrival the button rises gently and the lid gives a little hop. Over the pill the words widen, the fill swells with a bounce and the lid jumps up and drops back bouncing, showing the dark inside of the box. At a click the words rise away in a blur and Wrapping… arrives, with a sheen running through it. Round the box a sheet of ivory paper appears, with slanted red stripes, a gold line and a row of dots, still open: the two ends rise and slap onto the sides with a small bounce, then the front and the back; the box squashes a little at each slap. The two halves of the top fold over, first the front one and then the back one, which lies on it with its shadow. The stripes run on from flap to flap as on a real sheet, and while a flap faces away its plain back shows. Then a red satin ribbon climbs the front and runs over the top, another crosses the lid and goes down the side, with the light running along the satin. On the knot a bow puffs out: two loops coming out of the knot, opening with their dark fold inside and flapping a little, two swallow-tailed tails that drop and swing, and a gleam running over the whole bow. The pill widens with a bounce on Gift wrapped and a small Remove pill. Another click pulls a tail: the loops slip back into the knot, the ribbon slides off, the halves of the top rise, the flaps open again and the paper goes; the lid gives a hop and the pill goes back to Add gift wrap · €4. With the keyboard the focus shows a ring and the lid says hello; Enter or Space wrap it.

How it works

A 3D box on a canvas

An orthographic camera looks at the box from above (27 degrees) and from the side (36 degrees): three vectors, VIEW towards the eye, RIGHT and UP on the screen, and a 3D point becomes a point on the screen with two dot products. The camera always sees the front, the right side and the top, so the box (the base and the lid, two blocks) is drawn without sorting anything: only the three faces in view, each with its colour lightened or darkened by the dot product of its normal with the lamp, a gradient towards the top and a fine edge. The lid has an extra height that a spring with gravity makes hop under the pointer.

The paper: a sheet folded by its hinges

The sheet is a flat net: the bottom, four flaps round it and two halves of the top attached to the front and the back. Each flap keeps its rectangle in the net (u along the width, v along the depth) and a reference point with two 3D vectors, which depend on the hinge angles: open by a degrees, the front runs along (0, cos a, sin a), and its half of the top turns a further g degrees on its upper edge. In an orthographic projection a rectangle stays a parallelogram, so a single affine transform (setTransform) carries the net's coordinates onto the screen exactly, and the print (a pattern in the sheet's units) runs on from flap to flap across the folds. The sign of the dot product of the normal with the eye says which side shows: the print or the plain back.

The order of drawing

No z-buffer: the back and left flaps are drawn before the box, the front and right ones after it, and the back half of the top moves from before to after once it is past 45 degrees, that is once it is over the box. The back half has its hinge three tenths higher, so closed it lies on the front one, and its free edge leaves a hairline of shadow. The ribbon is drawn last, in strips on the closed faces, with a gradient across it (dark at the edges, a streak of light) that makes the satin: its run is a length that climbs the front and then crosses the top, and another that crosses the top and goes down the side.

The bow in SVG

The bow is an SVG over the canvas, drawn again every frame: each loop is a drop of two Bézier curves leaving the knot, with its darker opening inside and a thread of light along its upper edge, the right and left mirrored and turned 15 degrees up; the tails are slightly curved strips with a swallow tail and the slanted cut of scissors. GSAP drives the loops' size, a spring (2.4 swings a second) their flapping and another (1.6) the tails' swing, kicked when the bow is tied or the pointer arrives. A white gradient running over the loops and the knot makes the gleam of the satin. To untie, a pull value shrinks the loops towards the knot while the right tail grows longer.

The timing and the states

A GSAP timeline moves the flaps' angles (power3.in, then a small slap of 3.5 degrees and an elastic squash of the whole box), the folds of the top, the ribbon's two runs and the bow; PACE changes its speed with timeScale. The drawing loop runs only while something moves: a fingerprint of all the values decides whether to draw again, and when the springs and the lid are still it goes to sleep. The button is a real button: aria-disabled while it wraps and unwraps, an aria-live region says Wrapping your gift…, Gift wrap added, €4. and Gift wrap removed., and once wrapped the aria-label says Gift wrapped. Remove gift wrap. Wrapping takes about three seconds, more than the 2.8 between two clicks of the preview: the second click falls in the wait and the finished parcel stays in view for a whole round.

Values to change

NameFileDefaultWhat it changes
LABELscript.jsAdd gift wrapThe words at rest. If they are too long for the room, the type gets smaller.
PRICEscript.js€4The price of the wrap, after the words (empty, no price); the aria-live region says it too.
BUSYscript.jsWrapping…The words while the paper folds and the ribbon runs.
DONEscript.jsGift wrappedThe words once the parcel is done.
REMOVEscript.jsRemoveThe small pill on the right: it says another click takes the wrap off.
PACEscript.js1The pace of the wrapping: 2 takes twice as long, 0.5 half.
OPENscript.js36How open the paper is when it arrives, in degrees: higher, the flaps arrive further out and travel further.
LOOPSscript.js1The size of the bow's loops and tails.
SPRINGscript.js1How lively the bow is: how much the loops flap and the tails swing when it is tied or the pointer arrives. 0 they keep still.
HOPscript.js1How high the lid hops under the pointer. 0 it stays put.

Try them in the Playground

When to use it

For the gift option of a cart or a checkout: gift wrapping, a greeting card, special packaging. It suits shops of things to give, cosmetics, books, chocolate and the holiday season. The wrapping is simulated: in a real shop the click adds the line to the cart, and the done state waits for the server's answer.

On phones

The pill at rest is about 255 pixels wide and the done one about 290: at 390 pixels they fit with room to spare, and if the words are longer than the room the type gets smaller. A tap squashes the fill and wraps; with no pointer the lid hops on arrival and when the paper goes. The box keeps its size.

With reduced motion

No folding, no running ribbon and no puffing bow: at a click the finished parcel fades in over the bare box, then the bow fades in, and Wrapping… and Gift wrapped fade in; the pill takes its new width while the words are gone. The lid does not hop and the pill does not swell (under the mouse the fill only lightens a little). Another click fades the paper and the bow away.

Pitfalls

  • SVG gradient stops written as stop-color="var(--satin)" stayed black: a presentation attribute does not read CSS variables. The satin colours sit in three CSS classes (stop-color in a rule), so the Playground can change them.
  • The sheet arrived open at 42 degrees with the halves of the top straight: the back flap, more than twenty units tall, rose out of the pill's round end onto the white page. Now it arrives at 36 degrees with the halves already folded to 50, and the canvas is clipped to the shape of the round end.
  • The · €4 price sat far from the words: the invisible copy of the wide words, which holds their room for the hover, adds some fifteen pixels. The price has a negative margin, and under the pointer the widened words come right up beside it.
  • In the first try the lid's hop under the pointer could not be seen: two and a half pixels, in a 60 pixel pill. The push went from 34 to 50 units a second, a jump of about five pixels that drops back with two bounces.

Libraries

GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.

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

Testing

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