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
- Paste the snippet just before
</head>. - Add
data-gift-box-opento an element and put the gift inside it: a gift card, an image or a few words. - 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-color | Box colour. Default #6a0dad. |
data-ribbon | Ribbon and bow colour. Default #f5c542. |
data-loop | Close 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.
<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><div data-gift-box-open style="font-size: 24px">
<img src="gift-card.png" alt="£20 gift card" width="120">
</div>