Typewriter Concepts

1. Overview

With the Typewriter you can create text based animations, which mimic a user typing in text. The Typewriter supports: multiple cursors, selection, arrow movement, backspace and mouse movements.

The Typewriters cursors, represented by the TypewriterCursor class, behave like cursors in a text editor. The Typewriter tries to mimic / simulate real life as much as possible.

Here are some example animations:

A. Sentences

A simple animation which moves through a set of predefined sentences:

View code
import { typewriterFromSentences } from '@uiloos/core';

const typewriterEl = document.getElementById('sentences-typewriter');

typewriterFromSentences(
  {
    sentences: [
      'Superman is the man of steel',
      'Supergirls real name is Kara Zor-El',
      'Batman is the dark knight',
      'Batman\s nemesis is called the Joker',
      'The Flash can run through time',
      'Wonder woman possesses the Lasso of Truth',
    ],
    repeat: true,
    repeatDelay: 2000,
    text: 'Wonder woman possesses the Lasso of Truth',
  },
  (typewriter) => {
    typewriterEl.textContent = typewriter.text;

    const cursorEl = document.createElement('span');
    cursorEl.id = 'sentences-typewriter-cursor';
    if (typewriter.cursors[0].isBlinking) {
      cursorEl.classList.add('blinking');
    }
    typewriterEl.append(cursorEl);
  }
);
<div id="sentences-typewriter"></div>
#sentences-typewriter {
  font-family: monospace;
  font-size: 26px;
  display: inline-block;
  margin-bottom: 16px;
}

#sentences-typewriter-cursor {
  border-right-width: 4px; 
  border-right-style: solid;
  border-color: green;
}

#sentences-typewriter-cursor.blinking { 
  animation: example-blink 0.75s step-start infinite;
}

@keyframes example-blink {
  from,
  to {
    border-color: transparent;
  }
  50% {
    border-color: green;
  }
}
B. Multiple cursors

A more complex animation showing of how multiple cursors can work together. The animation praises a fictional collaboration product, called "collab".

View code
import { Typewriter } from '@uiloos/core';

const typewriterEl = document.getElementById('multicursor-typewriter');

function subscriber(typewriter) {
  typewriterEl.innerHTML = '';

  for (const position of typewriter) {
    // If there are multiple cursors, the last one will be on top
    for (const cursor of position.cursors.reverse()) {
      const color = cursor.data.color;

      const cursorEl = document.createElement("span");
      cursorEl.classList.add("cursor");
      cursorEl.style.setProperty("--cursor-color", color);

      if (cursor.isBlinking) {
        cursorEl.classList.add("blink");
      }

      typewriterEl.append(cursorEl);

      const infoEl = document.createElement("span");
      infoEl.className = "info";
      infoEl.style.setProperty("--cursor-color", color);
      infoEl.textContent = cursor.data.name;

      typewriterEl.append(infoEl);
    }


    const letterEl = document.createElement('span');
    letterEl.className = 'letter';
    letterEl.textContent = position.character;

    typewriterEl.append(letterEl);


    for (const cursor of position.selected.reverse()) {
      // This span has one or multiple cursors, the last one will win.
      const color = cursor.data.color;
      letterEl.style.setProperty('--background-color', color + '30'); // 30 = opacity
    }
  }
}

const config = {
  blinkAfter: 250,
  repeat: true,
  repeatDelay: 10000,
  cursors: [
    {
      position: 0,
      data: {
        name: 'Jim',
        color: '#ef4444',
      }
    },
    {
      position: 0,
      data: {
        name: 'Dwight',
        color: '#d946ef',
      }
    },
    {
      position: 0,
      data: {
        name: 'Pam',
        color: '#22c55e',
      }
    },
    {
      position: 0,
      data: {
        name: 'Michael',
        color: '#3b82f6',
      }
    },
  ],
  actions: [
    {
      type: 'keyboard',
      cursor: 0,
      text: 'W',
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'i',
      delay: 87,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 141,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'h',
      delay: 76,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 99,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'c',
      delay: 44,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 79,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'l',
      delay: 30,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'l',
      delay: 113,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'a',
      delay: 80,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'b',
      delay: 44,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 72,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'y',
      delay: 64,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 87,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'u',
      delay: 56,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 80,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'c',
      delay: 88,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'a',
      delay: 84,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'n',
      delay: 59,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 73,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'c',
      delay: 8,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'r',
      delay: 44,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 76,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'a',
      delay: 108,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 47,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 133,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'w',
      delay: 177,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'i',
      delay: 102,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'k',
      delay: 160,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'i',
      delay: 152,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 's',
      delay: 116,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ',',
      delay: 124,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 83,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'd',
      delay: 53,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 64,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'c',
      delay: 125,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'u',
      delay: 108,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'm',
      delay: 55,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 109,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'n',
      delay: 87,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 's',
      delay: 96,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ',',
      delay: 75,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 40,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'p',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'r',
      delay: 106,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 's',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 139,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'n',
      delay: 132,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 104,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'a',
      delay: 55,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 160,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'i',
      delay: 78,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 61,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'n',
      delay: 44,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 's',
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ',',
      delay: 44,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 84,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'a',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'n',
      delay: 99,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'd',
      delay: 84,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 61,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'p',
      delay: 124,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'r',
      delay: 151,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 105,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'j',
      delay: 60,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 49,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'c',
      delay: 88,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 158,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'z',
      delay: 88,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: ' ',
      delay: 156,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 43,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'o',
      delay: 63,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'g',
      delay: 33,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 144,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 't',
      delay: 87,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'h',
      delay: 43,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'e',
      delay: 96,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: 'r',
      delay: 69,
    },
    {
      type: 'keyboard',
      cursor: 0,
      text: '.',
      delay: 43,
    },
    {
      type: 'mouse',
      cursor: 1,
      position: 5,
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 1,
      text: '"',
      delay: 500,
    },
    {
      type: 'mouse',
      cursor: 1,
      position: 12,
      delay: 34,
    },
    {
      type: 'keyboard',
      cursor: 1,
      text: '"',
      delay: 500,
    },
    {
      type: 'mouse',
      cursor: 1,
      position: 49,
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 1,
      text: 'e',
      delay: 500,
    },
    {
      type: 'mouse',
      cursor: 1,
      position: 74,
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 1,
      text: '⌫',
      delay: 500,
    },
    {
      type: 'keyboard',
      cursor: 1,
      text: 's',
      delay: 66,
    },
    {
      type: 'mouse',
      cursor: 2,
      position: 45,
      selection: {
        start: 36,
        end: 45,
      },
      delay: 70,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'd',
      delay: 1000,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'o',
      delay: 178,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'c',
      delay: 101,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 's',
      delay: 73,
    },
    {
      type: 'mouse',
      cursor: 2,
      position: 55,
      selection: {
        start: 42,
        end: 55,
      },
      delay: 76,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 's',
      delay: 1000,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'l',
      delay: 131,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'i',
      delay: 122,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'd',
      delay: 88,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 'e',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: 's',
      delay: 160,
    },
    {
      type: 'mouse',
      cursor: 2,
      position: 72,
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: '⌫',
      delay: 150,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: ' ',
      delay: 82,
    },
    {
      type: 'keyboard',
      cursor: 2,
      text: '❤️',
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: ' ',
      delay: 50,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: '⌫',
      delay: 50,
    },
    {
      type: 'mouse',
      cursor: 3,
      position: 74,
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: ' ',
      delay: 125,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'N',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'o',
      delay: 77,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'w',
      delay: 92,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: ' ',
      delay: 65,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'w',
      delay: 100,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'i',
      delay: 86,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 't',
      delay: 53,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'h',
      delay: 87,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: ' ',
      delay: 136,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'A',
      delay: 78,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: 'I',
      delay: 114,
    },
    {
      type: 'keyboard',
      cursor: 3,
      text: '!',
      delay: 44,
    },
  ],
  repeat: false,
  repeatDelay: 0,
  autoPlay: true,
};

new Typewriter(config, subscriber);
<div id="multicursor-typewriter"></div>
/* Note: --cursor-color, and --background-color are set from JavaScript */

#multicursor-typewriter {
  position: relative;
  font-size: 32px;
  display: inline-block;
  width: 100%;
  min-height: 150px;
  margin-bottom: 8px;
}

#multicursor-typewriter .info {
  position: absolute;
  display: inline;
  margin-top: -20px;
  margin-left: -1px;
  padding: 4px;
  font-family: sans-serif;
  font-weight: bold;
  font-size: 12px;
  background-color: var(--cursor-color);
  color: white;
}

#multicursor-typewriter .letter {
  background-color: var(--background-color);
  padding: 0;
}

#multicursor-typewriter .cursor {
  display: inline-block;
  position: absolute;
  width: 2px;
  height: 32px;
  margin-left: -1px;
  margin-top: 6px;
  background-color: var(--cursor-color);
}

#multicursor-typewriter .blink {
  animation: multicursor-blink 1s step-start infinite;
}

@keyframes multicursor-blink {
  from,
  to {
    background-color: transparent;
  }
  50% {
    background-color: var(--cursor-color);
  }
}
C. Word by word

This animation animates word by word, instead of character by character:

View code
import { Typewriter } from '@uiloos/core';

const typewriterEl = document.getElementById('word-by-word-typewriter');

function subscriber(typewriter) {
  typewriterEl.innerHTML = typewriter.text;
}

const config = {
  "repeat": true,
  "repeatDelay": 10000,
  "actions": [
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "This",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "summer",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "experience",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "a",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "film",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "like",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "never",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "before",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": ":",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "Vampires",
      "delay": 1000
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "from",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "Venus",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "VII:",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "the",
      "delay": 2000
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": " ",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": "Quickening",
      "delay": 50
    },
    {
      "type": "keyboard",
      "cursor": 0,
      "text": ".",
      "delay": 50
    }
  ],
}

new Typewriter(config, subscriber);
<div id="word-by-word-typewriter"></div>
#word-by-word-typewriter {
  font-weight: bold;
  min-height: 100px;
  font-size: 22px;
  background: linear-gradient(#D91A1A, #333);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
D. Word for word

This animation animates word for word one word at a time.

View code
import { Typewriter } from '@uiloos/core';

const typewriterEl = document.getElementById('word-for-word-typewriter');

function subscriber(typewriter, event) {
  if (event.action) {
    typewriterEl.textContent = `🎵 ${event.action.text} 🎵`;
  }
}

const config = {
  repeat: true,
  repeatDelay: 10000,
  autoPlay: true,
  actions: [
    { type: 'keyboard', cursor: 0, text: 'Twinkle', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'twinkle', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'little', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'star', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'How', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'I', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'wonder', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'what', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'you', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'are!', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'Up', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'above', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'the', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'world', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'so', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'high.', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'Like', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'a', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'diamond', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'in', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'the', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'sky', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'Twinkle,', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'twinkle', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'little', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'star', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'How', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'I', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'wonder', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'what', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'you', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'are!', delay: 500 },
  ]
};

new Typewriter(config, subscriber);
<div id="word-for-word-typewriter"></div>
#word-for-word-typewriter {
  text-align: center; 
  font-weight: bold;
  font-size: 32px;
}
E. Karaoke

This animates a sentence like a karaoke machine:

Turn around bright eyes! Every now and then I fall apart!
View code
import { Typewriter } from '@uiloos/core';

const typewriterEl = document.getElementById('karaoke-typewriter-highlight');

function subscriber(typewriter) {
  typewriterEl.textContent = typewriter.text;
}

const config = {
  repeat: true,
  repeatDelay: 5000,
  actions: [
    { type: 'keyboard', cursor: 0, text: 'Turn ', delay: 400 },
    { type: 'keyboard', cursor: 0, text: 'around ', delay: 300 },
    { type: 'keyboard', cursor: 0, text: 'bright ', delay: 500 },
    { type: 'keyboard', cursor: 0, text: 'eyes! ', delay: 400 },
    { type: 'keyboard', cursor: 0, text: 'Every ', delay: 1000 },
    { type: 'keyboard', cursor: 0, text: 'now ', delay: 400 },
    { type: 'keyboard', cursor: 0, text: 'and ', delay: 100 },
    { type: 'keyboard', cursor: 0, text: 'then ', delay: 100 },
    { type: 'keyboard', cursor: 0, text: 'I ', delay: 100 },
    { type: 'keyboard', cursor: 0, text: 'fall ', delay: 100 },
    { type: 'keyboard', cursor: 0, text: 'apart!', delay: 100 },
  ],
  
};

new Typewriter(config, subscriber);
<div id="karaoke-typewriter">
  <span id="karaoke-typewriter-highlight"></span>
  <span id="karaoke-typewriter-regular">Turn around bright eyes! Every now and then I fall apart!</span>
</div>
#karaoke-typewriter {
  position: relative;
  text-align: left; 
  font-weight: bold;
  font-size: 32px;
}

#karaoke-typewriter-regular {
  color: #94a3b8;
}

#karaoke-typewriter-highlight {
  position: absolute;
  color: #5b21b6;
}

2. Initialization

A Typewriter can be initialized by calling the constructor. The constructor takes two arguments the config and an optional subscriber.

The config allows you to tell the Typewriter how many history items it should track, see 8. History for more information.

The second argument is an optional subscriber, the subscriber is a callback function, allowing you to observe changes of the Typewriter. When using vanilla JavaScript the callback is the place to perform any DOM manipulations. The callback receives the event that occurred so you can act on it.

When using reactive frameworks such as React, Vue, Angular or Svelte etc etc. The subscriber is not necessary since your framework of choice will do the heavy lifting of syncing the state of the Typewriter with the DOM. For more information see "Usage with Frameworks"

Initialization code example
import { Typewriter } from '@uiloos/core';

const config = {
  actions: [{
    type: 'keyboard',
    cursor: 0,
    text: 'a',
    delay: 128
  },{
    type: 'keyboard',
    cursor: 0,
    text: 'b',
    delay: 64
  },{
    type: 'keyboard',
    cursor: 0,
    text: 'c',
    delay: 32
  }],
};

function subscriber(typewriter, event) {
  console.log(event);
}

const typewriter = new Typewriter(config, subscriber)
import { Typewriter, TypewriterEvent } from '@uiloos/core';

const config = {
  actions: [{
    type: 'keyboard',
    cursor: 0,
    text: 'a',
    delay: 128
  },{
    type: 'keyboard',
    cursor: 0,
    text: 'b',
    delay: 64
  },{
    type: 'keyboard',
    cursor: 0,
    text: 'c',
    delay: 32
  }],
};

function subscriber(
  typewriter: Typewriter,
  event: TypewriterEvent
) {
  console.log(event);
}

const typewriter = new Typewriter(config, subscriber);

3. Live properties

The Typewriter tracks the status of itself in "live" properties. These live properties will sync automatically whenever you perform an action (call a method) of the Typewriter, such as play / pause or stop.

In other words: each time you call a method to alter the Typewriter, all "live" properties will have been updated to reflect the current status.

Since the Typewriter is an animation it will also update all properties whenever the animation performs an action / frame.

  1. text which is the current text the typewriter animation should display. Each part of the animation, called an action, will cause the text to change.

    You are supposed to take the text and put it inside of a DOM element such as a p, or a span.

  2. isPlaying tracks whether the animation is currently playing.

  3. isFinished tracks if the `Typewriter` has finished playing the entire animation.

  4. hasBeenStoppedBefore tracks if the animation was ever stopped during this animation cycle. It will reset when the animation is repeated. See 8. Repeat for more information.

  5. lastPerformedAction the last TypewriterAction which was performed.

4. Creating animations

There are 3 ways to create animations:

  1. By using the Typewriter Composer. The composer is a visual editor which allows you to create animations, by using your own keyboard and mouse and by acting out the animation.

    It is the preferred way to create animations.

  2. If you only want a simple Typewriter with a single cursor, which enters a bunch of predefined sentences, you can use the typewriterFromSentences.

    It is a function which creates a Typewriter from an array of strings.

  3. By manually configuring the actions within the config. This method is rather time consuming, and using the composer is recommended. See 5. Actions for more information.

5. Actions

This section explains how the "actions" of a Typewriter work, so you can create the animations manually. If you plan on using the Typewriter Composer you can skip this section safely.

The Typewriter will base the animations on the given actions array from the config. The actions array is in fact the animation itself.

Within the actions array each item should conform to the TypewriterAction type.

An action is one of two things: a TypewriterActionKeyboard or a TypewriterActionMouse. Respectively Representing keyboard clicks and mouse movements.

Lets take the following definition for example:

const actions = [{
  type: 'keyboard',
  cursor: 0,
  text: 'a',
  delay: 50
}, {
  type: 'keyboard',
  cursor: 0,
  text: 'b',
  delay: 50
}, {
  type: 'keyboard',
  cursor: 0,
  text: 'c',
  delay: 50
}, {
  type: 'mouse',
  cursor: 1,
  position: 0,
  selection: { start: 0, end: 3},
  delay: 1000
}, {
  type: 'keyboard',
  cursor: 1,
  text: '⌫',
  delay: 50
}, {
  type: 'keyboard',
  cursor: 1,
  text: 'xyz',
  delay: 50
}];

In the animation above, the letters "a", "b" and "c" will by typed by the first cursor, each letter will be entered after a 50 second delay.

Then a second cursor comes along after one second and selects the entire text. Then it presses "backspace" removing the text. Finally the second cursor will type in "xyz" instantaneously, as a single word.

Hopefully the above actions are relatively intuitive, but lets clarify some of it:

5.1 Understanding the mental model

The Typewriter simulates a text editor, and it is important to understand the model when creating animations manually.

The biggest difference between a text editor and the Typewriter is that it only works in one dimension. There are no "columns" / "multiple lines", there is only one "line", and you can move to the left or right, but not up and down.

One way to look at the actions array is that it a recording of one or multiple cursors acting in the same text editor. What the Typewriter does is play this recording, resulting in an animation.

The animation will always be the same given the same actions array. In other words: the Typewriter is deterministic (and not random). This guarantees that everyone sees the same animation every time.

Because the animation must be deterministic, all actions need to happen sequentially. So unlike in real life, it is therefore not possible to have multiple cursors perform an action at the exact same time.

Another important part is understanding that an action can affect one or more cursors position and selection.

When a cursors types a letter that cursors position is increased by one, as well as all cursors on the right or the typing cursor. If a cursor types into another cursors selection, the selection will grow. If a cursors clears all text, every cursors position is set to 0. The list goes on and on.

The behaviors were "divined" by experimenting in "google docs" and "vscode".

5.2 Special keys

There are 6 types of special keys, which when used as the text of a TypewriterActionKeyboard will perform a special action:

  1. '⌫' represents a backspace. It will when nothing is selected delete the previous character, and when the cursor does have a selection, remove all characters in the selection.

  2. '⎚'' represents 'Clear all', it clears the entire text.

  3. '←' represents the left arrow key. When nothing is selected is will move the cursor one position to the left. When a selection is made it will move the cursor to the start of the selection.

  4. '→' represents the right arrow key. When nothing is selected is will move the cursor one position to the right. When a selection is made it will move the cursor to the end of the selection.

  5. '⇧←' represents select left, when repeated grows the selection.

  6. '⇧→' represents select right, when repeated grows the selection.

5.3 Whole words vs single characters

The text of a TypewriterActionKeyboard can be any string. This allows a cursor to type in a complete word or sentence in one action.

5.4. Selections

A cursors can make selections via actions in two ways, the first is using a TypewriterActionMouse with a selection value.

The second is using the special keys: '⇧←' or '⇧→' as the text for for a TypewriterActionKeyboard.

A selection is represented by a TypewriterCursorSelection object, which has two properties both numbers: start and end.

A cursors selection vanishes whenever an actions is performed which is are not '⇧←' or '⇧→'.

What happens with the selection is dependant on the next action. It will be removed when "backspace" is pressed next, when a text is entered it will replace the selection etc etc.

5.5 Cursors

Within a TypewriterAction the cursor property is always a number. The number being an index within the cursors array of the TypewriterConfig object.

All cursors must be defined ahead of time when configuring the Typewriter. You cannot add cursors after the fact.

5.6 Validity

When a Typewriter is initialized with an actions array, it is very aggressively checked whether or not the animation is valid or not.

For example you are not allowed to make selections which are out of the bounds of the text, or set a selection and not have the cursors position be either the start or the end of that selection.

Check the API for TypewriterXXXError for types of errors.

5.7 Unicode / Emoji

The Typewriter supports unicode / emoji by treating all unicode symbols as a single character.

For example if you want to move over an emoji character you can simply use the special key , and the cursor will hop over the emoji.

There is however one note: when the text of a Typewriter contains an emoji the text.length can be a bit deceiving.

Let me explain: in JavaScript executing '👨‍👩‍👧‍👦'.length results in 11, and not 1. The reason for this is because JavaScript is answering the question: "how many code units are in the string", and not "how many characters are in the string".

So keep this in mind when using emoji / unicode and you are good to go.

6. AutoPlay

A Typewriter will by default play the animation automatically, if there are actions.

It can however can be configured to only start playing when the play() method is called by setting autoPlay to false.

The Typewriter can be paused via pause() and then resumed again via play(). When resumed it will take into account the duration that had already passed. For example: if the animation runs for 1000 milliseconds, and the user pauses after 800 milliseconds, when resumed the animation will run for another 200 milliseconds. This is because there was 200 milliseconds remaining for the animation.

The Typewriter can also be stopped via stop(). The difference between stop() and pause(), is that when play() is called after stop() the animation is restarted. For example: if the animation runs for 500 milliseconds, and the user stops after 250 milliseconds, when play is called, the animation will continue for 1000 milliseconds. This is because the animation is reset and not remembered.

7. Repeat

The Typewriter will by default run the configured animation a single time. But this can be configured via repeat to repeat a specific number of times, or indefinitely / forever.

Setting repeat to true makes it repeat forever. Setting it to a number makes it repeat for that amount of times.

By default the Typewriter will start repeating the animation straight away. It is possible to change this by setting the repeatDelay to a number in milliseconds. This way the Typewriter will show the final state of the previous iteration a little longer.

When using repeat in combination with typewriterFromSentences it is advised to configure the initial text to be the last sentence. This way there is a fluid transition between the last

8. Iterator

The Typewriter instance is an Iterator meaning, you can use the Typewriter in a for-of loop.

When you iterate over the Typewriter you iterate over all "positions" inside of the text. A position is represented by a type called TypewriterPosition.

A TypewriterPosition knows which character is on that position, all cursors that are on that position, and all cursors which have selected that position.

This is especially handy when working with multiple cursors, and selections. Because this allows you to visualize not only the "character" at that position, but also all cursors and selections.

See the tutorial to see the iterator in action.

9. History

The Typewriter can keep track of all events that occurred, by default is it configured not to keep any history.

When you set keepHistoryFor to a number, the Typewriter will make sure the history array will never exceed that size.

If the size is exceeded the oldest event is removed to make place for the new event. This is based on the first in first out principle.

Tutorials
Learn how to style a Typewriter