Email Compose
An email writes itself. The address types in and turns into a contact chip, the subject follows, then the body types paragraph by paragraph. Send is pressed, the window flies off, and a “Message sent” note pops up.
Hi Sam,
It’s up! Have a look and tell me what you think of the animations.
Thanks, Alex
How to use it
- Paste the snippet just before
</head>. - Add
data-email-composeto a container with one element per body paragraph, and setdata-toanddata-subject.
Options
Add any of these attributes to the same element.
data-to | The recipient’s address. |
data-subject | The subject line. |
data-title | The window’s title. Default “New message”. |
data-sent | The note shown after sending. Default “Message sent”. |
data-speed | Typing time per character in the body. Default 32 ms. |
data-loop | Write it all again after a pause. |
data-hold | How long the “sent” note stays before looping. Default 2200 ms. |
The window keeps its finished size from the start, so nothing around it moves while it types.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Email Compose: an email writes itself. The recipient types in and turns
// into a contact chip, the subject follows, the body types paragraph by
// paragraph, then Send is pressed and the message flies off with a
// "Message sent" note.
// Usage:
// <div data-email-compose data-to="[email protected]" data-subject="The new site is live">
// <p>Hi Sam,</p>
// <p>It's up! Have a look and tell me what you think.</p>
// </div>
// One element per body paragraph.
// Optional: data-to (recipient), data-subject, data-title (window title,
// default "New message"), data-sent (note after sending, default "Message
// sent"), data-speed (ms per character in the body, default 32), data-loop
// (write it again after a pause), data-hold (ms before looping, default 2200).
var DEFAULTS = { to: '[email protected]', subject: 'Hello', title: 'New message', sent: 'Message sent', speed: 32, hold: 2200 };
var css =
'.emc{position:relative;text-align:left}' +
'.emc-win{background:#fff;color:#1f2328;border-radius:12px;overflow:hidden;border:1px solid rgba(0,0,0,.08);box-shadow:0 22px 48px -14px rgba(40,10,70,.35)}' +
'.emc-top{display:flex;align-items:center;gap:0.6em;padding:0.65em 1em;background:#2b2d33;color:#fff;font-size:0.9em;font-weight:600}' +
'.emc-top span{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.emc-top i{width:0.75em;height:2px;background:#fff;opacity:.6;border-radius:1px}' +
'.emc-top i+i{height:0.75em;width:0.75em;background:none;border:2px solid #fff;border-radius:2px;opacity:.6;box-sizing:border-box}' +
'.emc-row{display:flex;align-items:center;gap:0.6em;min-height:2.5em;padding:0.3em 1em;border-bottom:1px solid #eceef1;white-space:nowrap;overflow:hidden}' +
'.emc-lab{flex:none;color:#8a8f98;width:4.4em}' +
'.emc-val{min-width:0;overflow:hidden;text-overflow:ellipsis}' +
'.emc-chip{display:inline-flex;align-items:center;gap:0.4em;padding:0.15em 0.6em 0.15em 0.2em;border-radius:99px;background:var(--accent-soft,#f3e8ff);max-width:100%}' +
'.emc-chip b{flex:none;width:1.6em;height:1.6em;border-radius:50%;display:grid;place-items:center;background:var(--accent,#6a0dad);color:#fff;font-size:0.75em}' +
'.emc-chip span{overflow:hidden;text-overflow:ellipsis}' +
'.emc-subj{font-weight:600}' +
'.emc-body{padding:0.8em 1em 0.4em;line-height:1.5}' +
'.emc-body p{margin:0 0 0.6em;min-height:1.5em}' +
'.emc-foot{display:flex;align-items:center;gap:0.8em;padding:0.4em 1em 0.9em}' +
'.emc-send{display:inline-flex;align-items:center;gap:0.45em;padding:0.55em 1.2em;border-radius:99px;background:var(--accent,#6a0dad);color:#fff;font-weight:600;font-size:0.95em}' +
'.emc-send svg{width:1em;height:1em}' +
'.emc-tools{display:flex;gap:0.7em;color:#9aa0a8}' +
'.emc-tools i{width:0.95em;height:0.95em;border-radius:3px;border:2px solid currentColor;box-sizing:border-box}' +
'.emc-tools i:nth-child(2){border-radius:50%}' +
'.emc-caret{display:inline-block;width:2px;height:1.15em;margin-left:1px;vertical-align:-0.2em;background:var(--accent,#6a0dad)}' +
'.emc-caret.emc-blink{animation:emc-blink 1.06s steps(1) infinite}@keyframes emc-blink{50%{opacity:0}}' +
'.emc-toasts{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none}' +
'.emc-toast{display:flex;align-items:center;gap:0.5em;padding:0.7em 1.2em;border-radius:99px;' +
'background:#1f2328;color:#fff;font-weight:600;white-space:nowrap;opacity:0;box-shadow:0 14px 30px rgba(0,0,0,.25)}' +
'.emc-toast b{display:grid;place-items:center;width:1.4em;height:1.4em;border-radius:50%;background:#22c55e;font-size:0.8em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var PLANE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4 20-7Z"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
function opt(d, k) { return d[k] !== undefined && d[k] !== '' ? d[k] : DEFAULTS[k]; }
function setup(el) {
var d = el.dataset;
var paras = Array.from(el.children).map(function (p) { return p.textContent.replace(/\s+/g, ' ').trim(); });
var to = opt(d, 'to'), subject = opt(d, 'subject');
el.setAttribute('role', 'img');
el.setAttribute('aria-label', 'An email to ' + to + ', subject "' + subject + '": ' + paras.join(' '));
el.textContent = '';
el.classList.add('emc');
var win = document.createElement('div');
win.className = 'emc-win';
win.setAttribute('aria-hidden', 'true');
win.innerHTML =
'<div class="emc-top"><span></span><i></i><i></i></div>' +
'<div class="emc-row"><span class="emc-lab">To</span><span class="emc-val emc-to"></span></div>' +
'<div class="emc-row"><span class="emc-lab">Subject</span><span class="emc-val emc-subj"></span></div>' +
'<div class="emc-body"></div>' +
'<div class="emc-foot"><span class="emc-send">Send ' + PLANE + '</span><span class="emc-tools"><i></i><i></i></span></div>';
win.querySelector('.emc-top span').textContent = opt(d, 'title');
var body = win.querySelector('.emc-body');
var ps = paras.map(function () { return body.appendChild(document.createElement('p')); });
var toast = document.createElement('div');
toast.className = 'emc-toast';
toast.setAttribute('aria-hidden', 'true');
toast.innerHTML = '<b>โ</b><span></span>';
toast.lastChild.textContent = opt(d, 'sent');
var toasts = document.createElement('div');
toasts.className = 'emc-toasts';
toasts.appendChild(toast);
el.appendChild(win);
el.appendChild(toasts);
var caret = document.createElement('span');
caret.className = 'emc-caret';
return {
el: el, win: win, toast: toast, caret: caret, to: to, subject: subject, paras: paras, ps: ps,
toEl: win.querySelector('.emc-to'), subjEl: win.querySelector('.emc-subj'), send: win.querySelector('.emc-send'),
visible: false, wake: null
};
}
function chip(s) {
var name = s.to.split('@')[0].replace(/[._-]+/g, ' ');
s.toEl.innerHTML = '<span class="emc-chip"><b></b><span></span></span>';
s.toEl.querySelector('b').textContent = name.charAt(0).toUpperCase();
s.toEl.querySelector('.emc-chip span').textContent = s.to;
return s.toEl.firstChild;
}
function finished(s) {
chip(s);
s.subjEl.textContent = s.subject;
s.ps.forEach(function (p, i) { p.textContent = s.paras[i]; });
}
function typeInto(s, node, text, speed) {
return new Promise(function (done) {
var n = 0;
var t = document.createTextNode('');
node.textContent = '';
node.appendChild(t);
node.appendChild(s.caret);
s.caret.classList.remove('emc-blink');
(function tick() {
t.data = text.slice(0, n);
if (n++ >= text.length) { done(); return; }
var ch = text.charAt(n - 1);
setTimeout(tick, speed * (0.5 + Math.random()) + (/[,.!?]/.test(ch) ? speed * 5 : 0));
})();
});
}
function whenVisible(s) {
return s.visible ? Promise.resolve() : new Promise(function (r) { s.wake = r; });
}
function play(s) {
var d = s.el.dataset;
var speed = +opt(d, 'speed');
s.toEl.textContent = '';
s.subjEl.textContent = '';
s.ps.forEach(function (p) { p.textContent = ''; });
anime.utils.set(s.win, { opacity: 1, x: 0, y: 0, rotate: 0, scale: 1 });
var chain = sleep(450)
.then(function () { return typeInto(s, s.toEl, s.to, 55); })
.then(function () { return sleep(250); })
.then(function () {
var c = chip(s);
anime.animate(c, { scale: [0.8, 1], duration: 380, ease: 'outBack(2)' });
return sleep(350);
})
.then(function () { return typeInto(s, s.subjEl, s.subject, 50); })
.then(function () { return sleep(300); });
s.ps.forEach(function (p, i) {
chain = chain.then(function () { return typeInto(s, p, s.paras[i], speed); })
.then(function () { return sleep(200); });
});
chain.then(function () {
s.caret.classList.add('emc-blink');
return sleep(600);
}).then(function () {
s.caret.remove();
return new Promise(function (r) {
anime.animate(s.send, { scale: [1, 0.9, 1], duration: 260, ease: 'inOut(2)', onComplete: r });
});
}).then(function () {
// Off it goes: up and away to the top right.
anime.animate(s.win, {
x: [0, 60], y: [0, -40], rotate: [0, 6], scale: [1, 0.5], opacity: [1, 0], duration: 620, ease: 'in(3)'
});
return sleep(450);
}).then(function () {
anime.animate(s.toast, { opacity: [0, 1], scale: [0.8, 1], duration: 420, ease: 'outBack(2)' });
if (d.loop === undefined || d.loop === 'false') return;
return sleep(+opt(d, 'hold')).then(function () { return whenVisible(s); }).then(function () {
anime.animate(s.toast, { opacity: 0, scale: 0.9, duration: 250, ease: 'in(2)' });
return sleep(300);
}).then(function () {
s.toEl.textContent = '';
s.subjEl.textContent = '';
s.ps.forEach(function (p) { p.textContent = ''; });
anime.utils.set(s.win, { x: 0, rotate: 0 });
anime.animate(s.win, { opacity: [0, 1], y: [12, 0], scale: [0.97, 1], duration: 450, ease: 'out(3)' });
return sleep(470);
}).then(function () { play(s); });
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._emc;
s.visible = e.isIntersecting;
if (s.visible && s.wake) { var w = s.wake; s.wake = null; w(); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-email-compose]').forEach(function (el) {
if (el._emc) return;
var s = (el._emc = setup(el));
finished(s);
if (reduced()) return;
// Keep the finished body's height (in em) so the window never grows while it types.
var body = s.win.querySelector('.emc-body');
var fs = parseFloat(getComputedStyle(body).fontSize) || 16;
if (body.offsetHeight) body.style.minHeight = (body.offsetHeight / fs) + 'em';
io.observe(el);
whenVisible(s).then(function () { play(s); });
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-email-compose data-to="[email protected]" data-subject="The new site is live">
<p>Hi Sam,</p>
<p>It's up! Have a look and tell me what you think.</p>
<p>Thanks, Alex</p>
</div>