← Typing

Typewriter

Text types itself in, one character at a time, with a blinking cursor and a touch of human unevenness. Give it several phrases and it types, erases and retypes each one.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-typewriter to a heading to type its text, or give it phrases separated by | to cycle through them.

Options

Add any of these attributes to the same element.

data-typewriter="A|B|C"Phrases to type, erase and retype in turn.
data-loopKeep retyping a single phrase.
data-speedTime per character. Default 60 ms.
data-delete-speedTime per character when erasing. Default 32 ms.
data-holdHow long a finished phrase stays. Default 1600 ms.
data-cursor="none"Hide the cursor.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Typewriter: text types itself in, character by character, with a blinking
    // cursor. Give it several phrases and it types, erases and retypes each one.
    // Usage: <h1 data-typewriter>Hello, world</h1>
    //    or: <h1 data-typewriter="Design|Build|Ship"></h1>  (cycles phrases)
    // Optional: data-speed (ms per character), data-delete-speed (ms), data-hold
    // (ms a phrase stays), data-loop (retype a single phrase), data-cursor ("none"
    // to hide it).
    var DEFAULTS = { speed: 60, deleteSpeed: 32, hold: 1600 };

    var css =
      '.tw-cursor{display:inline-block;width:0.08em;height:1em;margin-left:0.06em;vertical-align:-0.12em;background:currentColor;border-radius:1px}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var list = el.getAttribute('data-typewriter');
      var phrases = (list ? list.split('|') : [el.textContent]).map(function (p) { return p.replace(/\s+/g, ' ').trim(); }).filter(Boolean);
      el.setAttribute('aria-label', phrases.join(', '));
      el.textContent = '';
      var text = document.createElement('span');
      text.setAttribute('aria-hidden', 'true');
      el.appendChild(text);
      var cursor = null;
      if (el.dataset.cursor !== 'none') {
        cursor = document.createElement('span');
        cursor.className = 'tw-cursor';
        cursor.setAttribute('aria-hidden', 'true');
        el.appendChild(cursor);
      }
      return { el: el, phrases: phrases, text: text, cursor: cursor };
    }

    function run(t) {
      var d = t.el.dataset;
      var speed = +d.speed || DEFAULTS.speed;
      var delSpeed = +d.deleteSpeed || DEFAULTS.deleteSpeed;
      var hold = +d.hold || DEFAULTS.hold;
      var cycle = t.phrases.length > 1 || (d.loop !== undefined && d.loop !== 'false');
      var blink = null;

      function blinkOn() {
        if (!t.cursor || blink) return;
        blink = anime.animate(t.cursor, { opacity: [1, 0], duration: 530, ease: 'linear', loop: true, alternate: true });
      }
      function blinkOff() {
        if (blink) { blink.revert(); blink = null; }
      }

      var index = 0;
      function type(phrase, i) {
        blinkOff();
        if (i <= phrase.length) {
          t.text.textContent = phrase.slice(0, i);
          // A little human unevenness: pauses after spaces and punctuation.
          var pause = /[ ,.!?]/.test(phrase[i - 1] || '') ? speed * 2.2 : speed * anime.utils.random(0.6, 1.4, 2);
          return setTimeout(function () { type(phrase, i + 1); }, pause);
        }
        blinkOn();
        if (cycle) setTimeout(function () { erase(phrase.length); }, hold);
      }
      function erase(i) {
        blinkOff();
        if (i >= 0) {
          t.text.textContent = t.phrases[index].slice(0, i);
          return setTimeout(function () { erase(i - 1); }, delSpeed);
        }
        index = (index + 1) % t.phrases.length;
        setTimeout(function () { type(t.phrases[index], 1); }, speed * 6);
      }
      blinkOn();
      setTimeout(function () { type(t.phrases[0], 1); }, 400);
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          run(e.target._tw);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-typewriter]').forEach(function (el) {
        if (el._tw) return;
        el._tw = setup(el);
        if (reduced) { el._tw.text.textContent = el._tw.phrases[0]; return; }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-typewriter>Hello, world</h1>

<!-- cycle through phrases: -->
<h1 data-typewriter="Design it|Build it|Ship it"></h1>