โ† Typing

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

  1. Paste the snippet just before </head>.
  2. Add data-email-compose to a container with one element per body paragraph, and set data-to and data-subject.

Options

Add any of these attributes to the same element.

data-toThe recipient’s address.
data-subjectThe subject line.
data-titleThe window’s title. Default “New message”.
data-sentThe note shown after sending. Default “Message sent”.
data-speedTyping time per character in the body. Default 32 ms.
data-loopWrite it all again after a pause.
data-holdHow 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.

1 ยท Snippet
<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>
2 ยท Your HTML
<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>