← E-commerce

Gift Wrap

A plain cardboard box gets wrapped. Patterned paper folds in from each side and down over the top, a ribbon runs round it both ways, and a bow ties itself on top: the knot pops, the loops spring out and the tails drop down.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty element with data-gift-wrap. Size it with font-size: it is 9em square.

Options

Add any of these attributes to the same element.

data-colorPaper colour. Default #e11d48.
data-patterndots, stripes or plain. Default dots.
data-ribbonRibbon and bow colour. Default #f5c542.
data-loopUnwrap and wrap again every few seconds while in view.

Nice next to a “Add gift wrap” checkbox: call animacious.giftWrap(element) when it is ticked. It fires a wrapped event when the bow is tied.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Gift Wrap: a plain cardboard box gets wrapped. Patterned paper folds in
    // from each side and down over the top, a ribbon runs round it both ways,
    // and a bow ties itself on top, its loops springing out and its tails
    // dropping down.
    // Usage: <div data-gift-wrap></div>
    // Optional: data-color (paper colour, default #e11d48), data-pattern
    // (dots, stripes or plain; default dots), data-ribbon (default #f5c542),
    // data-loop (unwrap and wrap again every few seconds while in view).
    // Size it with font-size: it is 9em wide and 9em tall.
    // Fires a "wrapped" event when the bow is tied. Script use:
    // animacious.giftWrap(el) wraps it again.
    var DEFAULTS = { color: '#e11d48', pattern: 'dots', ribbon: '#f5c542', hold: 2200, gap: 700 };

    var css =
      '.gwr{position:relative;display:block;width:9em;height:9em;max-width:100%}' +
      '.gwr-box{position:absolute;left:.5em;right:.5em;bottom:.3em;height:5.6em;perspective:28em}' +
      '.gwr-card{position:absolute;inset:0;border-radius:.3em;background:linear-gradient(90deg,transparent 44%,rgba(255,255,255,.35) 44% 56%,transparent 56%),#c89b6d;' +
      'box-shadow:inset 0 -.4em 0 rgba(0,0,0,.08),0 .5em 1em rgba(15,15,30,.15)}' +
      '.gwr-card::before{content:"";position:absolute;left:0;right:0;top:0;height:1.1em;border-radius:.3em .3em 0 0;background:rgba(255,255,255,.18);border-bottom:1px solid rgba(0,0,0,.1)}' +
      '.gwr-flap{position:absolute;top:0;bottom:0;backface-visibility:hidden;background:var(--gwr-p);background-color:var(--gwr-c)}' +
      '.gwr-left{left:0;width:52%;border-radius:.3em 0 0 .3em;transform-origin:0 50%}' +
      '.gwr-right{right:0;width:52%;border-radius:0 .3em .3em 0;transform-origin:100% 50%;box-shadow:-.15em 0 .25em rgba(0,0,0,.18)}' +
      '.gwr-top{left:0;right:0;top:0;bottom:auto;height:1.4em;border-radius:.3em .3em 0 0;transform-origin:50% 0;box-shadow:0 .15em .25em rgba(0,0,0,.18)}' +
      '.gwr-shade{position:absolute;inset:0;border-radius:.3em;pointer-events:none;background:linear-gradient(90deg,rgba(0,0,0,.08),transparent 20% 80%,rgba(0,0,0,.14));opacity:0}' +
      '.gwr-rv,.gwr-rh{position:absolute;background:var(--gwr-r);box-shadow:0 0 0 1px rgba(0,0,0,.06)}' +
      '.gwr-rv{left:50%;top:0;bottom:0;width:1em;margin-left:-.5em;transform-origin:50% 0}' +
      '.gwr-rh{left:0;right:0;top:55%;height:1em;margin-top:-.5em;transform-origin:0 50%}' +
      '.gwr-bow{position:absolute;left:50%;bottom:4.95em;width:5em;margin-left:-2.5em}' +
      '.gwr-bow svg{display:block;width:100%;overflow:visible}' +
      '.gwr-bow .gwr-part{transform-box:fill-box}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var PATTERNS = {
      dots: 'radial-gradient(circle,rgba(255,255,255,.6) 0 .16em,transparent .18em) 0 0/.9em .9em,radial-gradient(circle,rgba(255,255,255,.35) 0 .1em,transparent .12em) .45em .45em/.9em .9em',
      stripes: 'repeating-linear-gradient(45deg,rgba(255,255,255,.4) 0 .3em,transparent .3em .8em)',
      plain: 'linear-gradient(transparent,transparent)'
    };

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function bowSvg() {
      // Loops pivot on the knot; tails hang from it.
      return '<svg viewBox="0 0 100 60" aria-hidden="true">' +
        '<path class="gwr-part gwr-tl" d="M47 40L34 58l6-1 3 3 6-19z" fill="var(--gwr-r)" style="transform-origin:100% 0"/>' +
        '<path class="gwr-part gwr-tr" d="M53 40l13 18-6-1-3 3-6-19z" fill="var(--gwr-r)" style="transform-origin:0 0"/>' +
        '<g class="gwr-part gwr-ll" style="transform-origin:100% 80%"><path d="M48 40C34 18 8 14 8 30c0 12 24 14 40 10z" fill="var(--gwr-r)"/>' +
        '<path d="M46 38C36 26 20 22 16 30" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/></g>' +
        '<g class="gwr-part gwr-lr" style="transform-origin:0 80%"><path d="M52 40C66 18 92 14 92 30c0 12-24 14-40 10z" fill="var(--gwr-r)"/>' +
        '<path d="M54 38C64 26 80 22 84 30" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/></g>' +
        '<rect class="gwr-part gwr-knot" x="42" y="32" width="16" height="14" rx="5" fill="var(--gwr-r)" stroke="rgba(0,0,0,.15)" stroke-width="2" style="transform-origin:50% 50%"/>' +
        '</svg>';
    }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('gwr');
      el.style.setProperty('--gwr-c', d.color || DEFAULTS.color);
      el.style.setProperty('--gwr-r', d.ribbon || DEFAULTS.ribbon);
      el.style.setProperty('--gwr-p', PATTERNS[d.pattern] || PATTERNS[DEFAULTS.pattern]);
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'A present being wrapped');
      el.setAttribute('role', 'img');
      el.innerHTML = '<div class="gwr-box"><span class="gwr-card"></span>' +
        '<span class="gwr-flap gwr-left"></span><span class="gwr-flap gwr-right"></span><span class="gwr-flap gwr-top"></span>' +
        '<span class="gwr-shade"></span><span class="gwr-rv"></span><span class="gwr-rh"></span></div>' +
        '<div class="gwr-bow">' + bowSvg() + '</div>';
      var q = function (c) { return el.querySelector(c); };
      var s = el._gwr = {
        left: q('.gwr-left'), right: q('.gwr-right'), top: q('.gwr-top'), shade: q('.gwr-shade'), rv: q('.gwr-rv'), rh: q('.gwr-rh'),
        ll: q('.gwr-ll'), lr: q('.gwr-lr'), knot: q('.gwr-knot'), tl: q('.gwr-tl'), tr: q('.gwr-tr'),
        busy: false, visible: false, wrapped: false
      };
      if (reduced()) { s.wrapped = true; return; }
      unwrapped(s);
      new IntersectionObserver(function (entries) {
        s.visible = entries[0].isIntersecting;
        if (s.visible && !s.busy && (!s.played || s.waiting)) { s.waiting = false; setTimeout(function () { wrap(el); }, 300); }
      }, { threshold: 0.3 }).observe(el);
    }

    function unwrapped(s) {
      anime.utils.set(s.left, { rotateY: 100 });
      anime.utils.set(s.right, { rotateY: -100 });
      anime.utils.set(s.top, { rotateX: -100 });
      anime.utils.set(s.shade, { opacity: 0 });
      anime.utils.set(s.rv, { scaleY: 0 });
      anime.utils.set(s.rh, { scaleX: 0 });
      anime.utils.set([s.ll, s.lr, s.knot], { scale: 0 });
      anime.utils.set([s.tl, s.tr], { scaleY: 0 });
    }

    function wrap(el) {
      var s = el._gwr, d = el.dataset;
      if (s.busy || s.wrapped) return;
      s.busy = true;
      s.played = true;
      var tl = anime.createTimeline({
        onComplete: function () {
          s.busy = false;
          s.wrapped = true;
          el.dispatchEvent(new CustomEvent('wrapped', { bubbles: true }));
          if (on(d.loop)) setTimeout(function () { unwrap(el); }, DEFAULTS.hold);
        }
      });
      // Paper folds in from each side, then down over the top.
      tl.add(s.left, { rotateY: [100, 0], duration: 650, ease: 'out(3)' }, 0)
        .add(s.right, { rotateY: [-100, 0], duration: 650, ease: 'out(3)' }, 380)
        .add(s.top, { rotateX: [-100, 0], duration: 550, ease: 'out(3)' }, 820)
        .add(s.shade, { opacity: [0, 1], duration: 400 }, 900)
        // Ribbon runs round both ways.
        .add(s.rv, { scaleY: [0, 1], duration: 420, ease: 'out(3)' }, 1350)
        .add(s.rh, { scaleX: [0, 1], duration: 420, ease: 'out(3)' }, 1620)
        // The bow ties itself: knot, loops spring out, tails drop.
        .add(s.knot, { scale: [0, 1.25, 1], duration: 380, ease: 'out(2)' }, 1980)
        .add(s.ll, { scale: [0, 1], rotate: [50, 0], duration: 650, ease: 'outElastic(1, .55)' }, 2120)
        .add(s.lr, { scale: [0, 1], rotate: [-50, 0], duration: 650, ease: 'outElastic(1, .55)' }, 2200)
        .add(s.tl, { scaleY: [0, 1], rotate: [20, 0], duration: 500, ease: 'outBack(2)' }, 2300)
        .add(s.tr, { scaleY: [0, 1], rotate: [-20, 0], duration: 500, ease: 'outBack(2)' }, 2340)
        .add(s.knot, { scale: [1, 0.85, 1], duration: 260, ease: 'inOut(2)' }, 2700);
    }

    function unwrap(el) {
      var s = el._gwr;
      if (!s.visible) { s.waiting = true; s.wrapped = false; unwrapped(s); return; }
      s.busy = true;
      var tl = anime.createTimeline({
        onComplete: function () {
          s.busy = false;
          s.wrapped = false;
          setTimeout(function () { if (s.visible) wrap(el); else s.waiting = true; }, DEFAULTS.gap);
        }
      });
      tl.add([s.ll, s.lr, s.knot, s.tl, s.tr], { scale: 0, duration: 300, ease: 'in(2)' }, 0)
        .add([s.rv], { scaleY: 0, duration: 300, ease: 'in(2)' }, 200)
        .add([s.rh], { scaleX: 0, duration: 300, ease: 'in(2)' }, 200)
        .add(s.shade, { opacity: 0, duration: 300 }, 400)
        .add(s.top, { rotateX: -100, duration: 400, ease: 'in(2)' }, 450)
        .add(s.right, { rotateY: -100, duration: 450, ease: 'in(2)' }, 650)
        .add(s.left, { rotateY: 100, duration: 450, ease: 'in(2)' }, 800)
        .call(function () { anime.utils.set([s.tl, s.tr], { scale: 1, scaleY: 0 }); }, 1260);
    }

    window.animacious = window.animacious || {};
    window.animacious.giftWrap = function (el) {
      var s = el && el._gwr;
      if (!s || s.busy) return;
      s.wrapped = false;
      unwrapped(s);
      wrap(el);
    };

    function init() {
      document.querySelectorAll('[data-gift-wrap]').forEach(function (el) {
        if (el._gwr) return;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-gift-wrap style="font-size: 20px"></div>

<!-- teal paper with stripes: -->
<div data-gift-wrap data-color="#0d9488" data-pattern="stripes"></div>