← E-commerce

Gift Box Open

A wrapped present gives an excited little shake, then the lid pops off and tumbles away. The gift inside rises out in a burst of sparkles and bobs gently. Click or tap the box to close it and open it again.

£20Gift card

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-gift-box-open to an element and put the gift inside it: a gift card, an image or a few words.
  3. Size it with font-size: the box is 8em wide and 10em tall, so keep the gift within about 7em by 3.5em.

Options

Add any of these attributes to the same element.

data-colorBox colour. Default #6a0dad.
data-ribbonRibbon and bow colour. Default #f5c542.
data-loopClose and open again every few seconds while in view.

It opens when scrolled into view and fires an open event; animacious.giftBoxOpen(element) opens it from code. It is a button for keyboard and screen reader users too.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Gift Box Open: a wrapped present gives a little shake, the lid pops off,
    // and the gift inside rises out in a burst of sparkles. Whatever you put
    // inside the element is the gift. Click or tap the box to open (or close)
    // it again.
    // Usage: <div data-gift-box-open><span>£20 gift card</span></div>
    // Optional: data-color (box colour, default #6a0dad), data-ribbon (ribbon
    // colour, default #f5c542), data-loop (close and open again every few
    // seconds while in view). Size it with font-size: the box is 8em wide.
    // Fires an "open" event once it is open. Script use:
    // animacious.giftBoxOpen(el) opens it.
    var DEFAULTS = { color: '#6a0dad', ribbon: '#f5c542', hold: 2400, gap: 900 };

    var css =
      '.gbo{position:relative;display:inline-block;width:8em;height:10em;cursor:pointer;-webkit-tap-highlight-color:transparent;vertical-align:middle}' +
      '.gbo-body{position:absolute;left:.4em;right:.4em;bottom:0;height:4.8em;border-radius:0 0 .45em .45em;z-index:2;transform-origin:50% 100%;' +
      'background:linear-gradient(90deg,transparent 42%,var(--gbo-r) 42% 58%,transparent 58%),linear-gradient(180deg,rgba(0,0,0,.18),rgba(0,0,0,0) 30%),var(--gbo-c);' +
      'box-shadow:inset -.5em 0 0 rgba(0,0,0,.1)}' +
      '.gbo-lid{position:absolute;left:0;right:0;bottom:4.6em;height:1.5em;border-radius:.35em;z-index:3;transform-origin:50% 100%;' +
      'background:linear-gradient(90deg,transparent 40%,var(--gbo-r) 40% 60%,transparent 60%),var(--gbo-c);' +
      'box-shadow:inset 0 -.3em 0 rgba(0,0,0,.14),0 .2em .5em rgba(0,0,0,.12)}' +
      '.gbo-bow{position:absolute;left:50%;bottom:1.25em;width:4.2em;margin-left:-2.1em}' +
      '.gbo-bow svg{display:block;width:100%;overflow:visible}' +
      '.gbo-gift{position:absolute;left:0;right:0;bottom:.8em;z-index:1;display:flex;justify-content:center;align-items:flex-end}' +
      '.gbo-spark{position:absolute;left:50%;bottom:4.8em;width:.7em;height:.7em;margin:-.35em;z-index:4;opacity:0;pointer-events:none;' +
      'background:var(--gbo-s);clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%)}' +
      '.gbo-shadow{position:absolute;left:.2em;right:.2em;bottom:-.35em;height:.7em;border-radius:50%;background:rgba(0,0,0,.12);filter:blur(3px)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var SPARKS = ['#f5c542', '#fff', '#f472b6', '#38bdf8', '#a3e635'];

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

    function bowSvg() {
      return '<svg viewBox="0 0 84 40" aria-hidden="true">' +
        '<path d="M42 30C30 10 6 2 6 18c0 12 22 14 36 12z" fill="var(--gbo-r)"/>' +
        '<path d="M42 30C54 10 78 2 78 18c0 12-22 14-36 12z" fill="var(--gbo-r)"/>' +
        '<path d="M42 29C32 17 16 11 13 19" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/>' +
        '<path d="M42 29C52 17 68 11 71 19" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/>' +
        '<rect x="35" y="22" width="14" height="13" rx="4" fill="var(--gbo-r)" stroke="rgba(0,0,0,.15)" stroke-width="2"/></svg>';
    }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('gbo');
      el.style.setProperty('--gbo-c', d.color || DEFAULTS.color);
      el.style.setProperty('--gbo-r', d.ribbon || DEFAULTS.ribbon);
      var gift = document.createElement('div');
      gift.className = 'gbo-gift';
      while (el.firstChild) gift.appendChild(el.firstChild);
      var shadow = document.createElement('span');
      shadow.className = 'gbo-shadow';
      var body = document.createElement('span');
      body.className = 'gbo-body';
      var lid = document.createElement('span');
      lid.className = 'gbo-lid';
      lid.innerHTML = '<span class="gbo-bow">' + bowSvg() + '</span>';
      [shadow, body, lid].forEach(function (x) { x.setAttribute('aria-hidden', 'true'); });
      el.appendChild(shadow);
      el.appendChild(gift);
      el.appendChild(body);
      el.appendChild(lid);
      var sparks = [];
      for (var i = 0; i < 12; i++) {
        var sp = document.createElement('span');
        sp.className = 'gbo-spark';
        sp.setAttribute('aria-hidden', 'true');
        sp.style.setProperty('--gbo-s', SPARKS[i % SPARKS.length]);
        el.appendChild(sp);
        sparks.push(sp);
      }
      var s = el._gbo = { gift: gift, body: body, lid: lid, sparks: sparks, box: [shadow, body, lid], open: false, busy: false, visible: false };
      anime.utils.set(gift, { y: 0, opacity: 0 });
      gift.setAttribute('aria-hidden', 'true');

      if (!el.closest('a,[aria-hidden="true"]')) {
        el.setAttribute('role', 'button');
        el.setAttribute('tabindex', '0');
        el.setAttribute('aria-label', 'Open the present');
        var toggle = function () {
          s.user = true;
          if (s.open) close(el); else open(el);
        };
        el.addEventListener('click', toggle);
        el.addEventListener('keydown', function (e) {
          if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
        });
      }

      if (reduced()) { open(el); return; }
      new IntersectionObserver(function (entries) {
        s.visible = entries[0].isIntersecting;
        if (s.visible && !s.open && !s.busy && !s.played) { s.played = true; setTimeout(function () { open(el); }, 300); }
        else if (s.visible && s.waiting) { s.waiting = false; cycle(el); }
      }, { threshold: 0.3 }).observe(el);
    }

    // With data-loop: close and open again, but only while it can be seen.
    function cycle(el) {
      var s = el._gbo;
      if (!on(el.dataset.loop) || s.user) return;
      if (!s.visible) { s.waiting = true; return; }
      if (s.open) close(el, function () { setTimeout(function () { cycle(el); }, DEFAULTS.gap); });
      else open(el);
    }

    function open(el) {
      var s = el._gbo;
      if (s.busy || s.open) return;
      s.busy = true;
      var em = parseFloat(getComputedStyle(el).fontSize);
      var done = function () {
        s.busy = false;
        s.open = true;
        s.gift.removeAttribute('aria-hidden');
        if (el.hasAttribute('role')) el.setAttribute('aria-label', 'Close the present');
        el.dispatchEvent(new CustomEvent('open', { bubbles: true }));
        setTimeout(function () { cycle(el); }, DEFAULTS.hold);
      };
      if (reduced()) {
        anime.utils.set(s.lid, { opacity: 0 });
        anime.utils.set(s.gift, { opacity: 1, y: -4.2 * em });
        done();
        return;
      }
      var tl = anime.createTimeline({ onComplete: done });
      // A couple of excited wiggles.
      tl.add(s.box, { x: [0, -.18 * em, .18 * em, -.12 * em, .12 * em, 0], duration: 560, ease: 'inOut(2)' }, 0)
        .add(s.lid, { rotate: [0, -5, 5, -3, 3, 0], duration: 560, ease: 'inOut(2)' }, 0)
        .add(s.body, { scaleY: [1, 0.9, 1], scaleX: [1, 1.06, 1], duration: 300, ease: 'out(2)' }, 560)
        // The lid pops up, tumbles to the side and fades.
        .add(s.lid, { y: [0, -3.4 * em, -1.6 * em], x: [0, 1.4 * em, 3.4 * em], rotate: [0, 18, 38], opacity: [1, 1, 0], duration: 900, ease: 'out(2)' }, 600)
        // The gift rises out.
        .add(s.gift, { y: [0, -4.2 * em], opacity: [0, 1], scale: [0.6, 1], duration: 760, ease: 'outBack(1.6)' }, 760);
      s.sparks.forEach(function (sp, i) {
        var ang = (-160 + (140 * i) / (s.sparks.length - 1)) * Math.PI / 180;
        var dist = (2.6 + Math.random() * 1.8) * em;
        tl.add(sp, {
          x: [0, Math.cos(ang) * dist], y: [0, Math.sin(ang) * dist], rotate: [0, 180],
          scale: [0.3, 1.2, 0], opacity: [1, 1, 0], duration: 900 + Math.random() * 300, ease: 'out(3)'
        }, 780 + Math.random() * 120);
      });
      tl.add(s.gift, { y: [-4.2 * em, -3.9 * em, -4.2 * em], duration: 1400, ease: 'inOut(2)' }, 1600);
    }

    function close(el, then) {
      var s = el._gbo;
      if (s.busy || !s.open) return;
      s.busy = true;
      var em = parseFloat(getComputedStyle(el).fontSize);
      var tl = anime.createTimeline({
        onComplete: function () {
          s.busy = false;
          s.open = false;
          s.gift.setAttribute('aria-hidden', 'true');
          if (el.hasAttribute('role')) el.setAttribute('aria-label', 'Open the present');
          if (then) then();
        }
      });
      tl.add(s.gift, { y: 0, opacity: 0, scale: 0.8, duration: 450, ease: 'in(2)' }, 0)
        .add(s.lid, { y: [-2.4 * em, 0], x: 0, rotate: 0, opacity: [0, 1], duration: 520, ease: 'outBounce' }, 300)
        .add(s.body, { scaleY: [1, 0.94, 1], duration: 260, ease: 'out(2)' }, 640);
    }

    window.animacious = window.animacious || {};
    window.animacious.giftBoxOpen = function (el) { if (el && el._gbo) open(el); };

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-gift-box-open style="font-size: 24px">
  <img src="gift-card.png" alt="£20 gift card" width="120">
</div>