
The paper calendar
Calendars, 3D, SVG
Free
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.
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>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
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 siteAn 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 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.
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 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.
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.
| Name | File | Default | What it changes |
|---|---|---|---|
LABEL | script.js | Add gift wrap | The words at rest. If they are too long for the room, the type gets smaller. |
PRICE | script.js | €4 | The price of the wrap, after the words (empty, no price); the aria-live region says it too. |
BUSY | script.js | Wrapping… | The words while the paper folds and the ribbon runs. |
DONE | script.js | Gift wrapped | The words once the parcel is done. |
REMOVE | script.js | Remove | The small pill on the right: it says another click takes the wrap off. |
PACE | script.js | 1 | The pace of the wrapping: 2 takes twice as long, 0.5 half. |
OPEN | script.js | 36 | How open the paper is when it arrives, in degrees: higher, the flaps arrive further out and travel further. |
LOOPS | script.js | 1 | The size of the bow's loops and tails. |
SPRING | script.js | 1 | How 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. |
HOP | script.js | 1 | How high the lid hops under the pointer. 0 it stays put. |
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.
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.
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.
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.
Tested on 12 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.