Envelope Open
The wax seal pops, the flap folds back and a letter slides up out of the envelope with a little spring. Loop it and the letter tucks back in, the flap closes and the seal stamps back on.
How to use it
- Paste the snippet just before
</head>. - Add
data-envelope-opento an empty<span>. Size it withfont-size: the envelope is 1em wide.
Options
Add any of these attributes to the same element.
data-text | A short line written on the letter, e.g. Hello!. |
data-color | Envelope colour. Default #8b5cf6. |
data-seal | Seal colour. Default #f43f5e; none for no seal. |
data-loop | Open, hold, close and repeat. |
data-hold | How long it stays open, when looping. Default 2000 ms. |
data-delay | Wait before it opens. |
It opens when scrolled into view. With reduced motion the letter simply shows, already out.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Envelope Open: the wax seal pops, the flap folds open and a letter slides
// up out of the envelope. Loop it and the letter tucks back in, the flap
// closes and the seal returns. Starts when scrolled into view.
// Usage: <span data-envelope-open></span> (size it with font-size)
// Optional: data-text (a short line written on the letter), data-color
// (envelope colour, default #8b5cf6), data-seal (seal colour, default
// #f43f5e, or "none"), data-loop (open, hold, close and repeat), data-hold
// (ms it stays open when looping, default 2000), data-delay (ms before it starts).
var DEFAULTS = { color: '#8b5cf6', seal: '#f43f5e', hold: 2000 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.env{display:inline-block;line-height:0}' +
'.env svg{display:block;width:1em;height:1.1em;overflow:visible}' +
'.env-flap{transform-origin:60px 62px}' +
'.env-seal{transform-origin:60px 90px}' +
'.env-whole{transform-origin:60px 124px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function esc(t) { return String(t).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
function build(el) {
var d = el.dataset;
var color = d.color || DEFAULTS.color;
var seal = d.seal || DEFAULTS.seal;
var text = d.text ? '<text x="60" y="80" text-anchor="middle" font-size="10" font-weight="700" fill="#1f2937" font-family="inherit">' + esc(d.text) + '</text>' : '';
var lines = d.text ? [88, 96, 104] : [78, 86, 94, 102];
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 10 120 116');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML =
'<g class="env-whole">' +
// Back of the envelope, a shade darker.
'<rect x="10" y="60" width="100" height="64" rx="5" fill="' + color + '"/>' +
'<rect x="10" y="60" width="100" height="64" rx="5" fill="#000" opacity=".28"/>' +
'<g class="env-letter"><rect x="20" y="66" width="80" height="54" rx="3" fill="#fff"/>' + text +
lines.map(function (y, i) { return '<rect x="30" y="' + y + '" width="' + (i === lines.length - 1 ? 34 : 60) + '" height="3" rx="1.5" fill="#d4d4d8"/>'; }).join('') +
'</g>' +
// The front pocket, with a V cut where the letter shows.
'<path d="M10 64 L60 98 L110 64 V119 a5 5 0 0 1 -5 5 H15 a5 5 0 0 1 -5 -5 Z" fill="' + color + '"/>' +
'<path d="M10 122 L52 92 M110 122 L68 92" stroke="#000" stroke-opacity=".12" stroke-width="1.5"/>' +
'<g class="env-flap"><path d="M12 61 L60 97 L108 61 Z" fill="' + color + '" stroke="' + color + '" stroke-width="4" stroke-linejoin="round"/>' +
'<path d="M12 61 L60 97 L108 61 Z" fill="#fff" opacity=".14"/></g>' +
(seal === 'none' ? '' : '<g class="env-seal"><circle cx="60" cy="90" r="7.5" fill="' + seal + '"/>' +
'<path d="M60 94 C55 90.5 55 86.5 57.6 86.5 C58.8 86.5 59.6 87.4 60 88.2 C60.4 87.4 61.2 86.5 62.4 86.5 C65 86.5 65 90.5 60 94 Z" fill="#fff" opacity=".9"/></g>') +
'</g>';
el.appendChild(svg);
return {
el: el, svg: svg,
whole: svg.querySelector('.env-whole'),
letter: svg.querySelector('.env-letter'),
flap: svg.querySelector('.env-flap'),
seal: svg.querySelector('.env-seal')
};
}
// Closed, the flap is on top of everything; open, it folds behind the letter.
function flapBehind(s, behind) {
var g = s.whole;
if (behind) g.insertBefore(s.flap, s.letter);
else g.insertBefore(s.flap, s.seal || null);
}
function showOpen(s) {
flapBehind(s, true);
anime.utils.set(s.flap, { scaleY: -1 });
anime.utils.set(s.letter, { y: -50 });
if (s.seal) anime.utils.set(s.seal, { scale: 0 });
}
function open(s) {
var tl = anime.createTimeline();
if (s.seal) tl.add(s.seal, { scale: [{ to: 1.25, duration: 140, ease: 'out(2)' }, { to: 0, duration: 200, ease: 'in(2)' }] });
tl.add(s.flap, { scaleY: 0, duration: 260, ease: 'in(2)' })
.call(function () { flapBehind(s, true); })
.add(s.flap, { scaleY: -1, duration: 420, ease: 'outBack(1.6)' })
.add(s.letter, { y: -50, duration: 760, ease: 'outBack(1.2)' }, '-=200')
.add(s.whole, { scaleY: [1, 0.96, 1], duration: 360, ease: 'out(2)' }, '<<');
return tl;
}
function close(s) {
var tl = anime.createTimeline();
tl.add(s.letter, { y: 0, duration: 560, ease: 'inOut(3)' })
.add(s.flap, { scaleY: 0, duration: 300, ease: 'in(2)' }, '-=120')
.call(function () { flapBehind(s, false); })
.add(s.flap, { scaleY: 1, duration: 260, ease: 'out(2)' });
if (s.seal) tl.add(s.seal, { scale: [0, 1], duration: 500, ease: 'outElastic(1, .6)' }, '-=80');
return tl;
}
function play(s) {
var d = s.el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var hold = +d.hold || DEFAULTS.hold;
var cycle = function () {
open(s);
if (!loop) return;
setTimeout(function () {
close(s);
setTimeout(cycle, 1900);
}, 1800 + hold);
};
setTimeout(cycle, +d.delay || 200);
}
function setup(el) {
el.classList.add('env');
el._env = build(el);
if (!el.getAttribute('aria-label') && !el.closest('[aria-hidden="true"]')) {
el.setAttribute('role', 'img');
el.setAttribute('aria-label', el.dataset.text ? 'A letter: ' + el.dataset.text : 'An envelope opening');
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target._env);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-envelope-open]').forEach(function (el) {
if (el._env) return;
setup(el);
if (reduced()) showOpen(el._env);
else io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><span data-envelope-open data-text="You're invited!" style="font-size: 160px"></span>