Ghost type is a small Javascript utility used to display automated typing.

The best way to illustrate what it does is simply to show some examples.

Tell a knock-knock joke
Animate the writing of a letter

Usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
// initialize the class passing it a base id,
// an array of nodes, and an options object (optional)
const ghostType = new GhostType(
  'base-id', // id  of the element where type will show up 
  [
    { 
      text: 'This is example text being typed',
      typingDelay: 100
    }
  ]
);
// call the instances start method
ghostType.start();

GhostType API

Key Description Type Required Default
anchorEl The id of the element within which the typing will be displayed String Yes none
nodes An array of nodes (node structure is explained below) Array Yes none
cursor An object controling features of the cursor Object No none
cursor.hide Specifies if cursor should be shown or not Boolean No False
cursor.color Sets cursors color (accepts all values that work in css) String No black
options An object controling various options Object No none
options.globalDelay Time delay after each charcter is typed. Passing a [min,max] array makes the delay dynamic. Int or [Int,Int] No 100
options.repeat Object specifying whether to and how to repeat. Object No none
options.repeat.clear Determines whether previous text should be cleared before repeating Boolean No False
options.repeat.count Specifies the number of times the nodes should be repeated. If left undefined the it will repeat infinitely Int No Infinity
options.onComplete A function that will be run after all of the nodes have completed. Function No none

Node Structure

Key Description Type Required Default
text string of text to be typed String No None
html string of html they will appear on screen all at once String No None
typingDelay time delay after each charcter is typed. Passing a [min,max] array makes the delay dynamic. Int or [Int,Int] No None
pause pause in microseconds before typing is started. Int No None
style a string that uses css styled syntax String No None
backspace.delay same as typingDelay
backspace.pause same as pause
backspace.count number of characters that get deleted after the nodes typing has completed. Int No Defaults to the length of text that was typed.
jumpBack Number of nodes to move back to before continuing. Does not clear already typed content. Int No none

The whole utility is run by a single class, the code for which is below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class GhostType {
  constructor({ anchorEl, nodes, options = {}, cursor: cursorSettings }) {
    this.nodes = nodes;
    this.index = 0;
    this.options = options;
    this.content = this.setUpContentSpan(anchorEl);
    this.cursor = this.setUpCursorSpan(anchorEl, cursorSettings);
  }

  // creates the span where the text/html will be typed 
  setUpContentSpan = (anchorEl) => {
    const content = document.createElement('span');
    document.getElementById(anchorEl).appendChild(content);
    return content;
  }

  // creates the span responsible for rendering the cursor 
  setUpCursorSpan = (anchorEl, cursorSettings = {}) => {
    if (cursorSettings.hide)
      return null;

    // Add keyframes to document for cursor flicker animation
    const cursorAnimation = document.createElement("style");
    const FLICKER = `flicker {
      0% { opacity: 1; }
      50% { opacity: 1; }
      75% { opacity: 0; }
      100% { opacity: 1; }
    }`;
    cursorAnimation.textContent = `
      @-webkit-keyframes ${FLICKER}
      @-moz-keyframes ${FLICKER}
      @-o-keyframes ${FLICKER}
      @keyframes ${FLICKER}
    `;
    document.body.appendChild(cursorAnimation);

    const cursor = document.createElement('span');
    cursor.innerText = '.';
    cursor.id = 'cursor';
    cursor.style = `
      display: inline-block;
      background-color: ${cursorSettings.color || 'black'};
      width: 2px;
      line-height: 1em;
      color: #ffffff00;
      animation: 1s linear flicker infinite;
    `;
    document.getElementById(anchorEl).appendChild(cursor);
    return cursor;
  }

  // used to add a span for each new node so that individual nodes
  // can have custom styling
  appendNewSpanToContent = (style) => {
    const span = document.createElement('span');
    span.style = style || '';
    this.content.appendChild(span);
    return span;
  }

  // Calculates a random delay duration between the provided [min, max]
  calculateRandomDelay = ([ min, max ]) => Math.floor(Math.random() * (max - min)) + min;

  // Finds delay based on node and global delay settings
  determineDelayDuration = (delay) => {
    // node has a delay set
    if (delay)
      return Array.isArray(delay) ? this.calculateRandomDelay(delay) : delay;
    
    const { globalDelay } = this.options;
    return Array.isArray(globalDelay) ? this.calculateRandomDelay(globalDelay) : globalDelay;
  }

  // types out the prompt for a given node with delays between 
  // each character based on specified values
  typeTextNode = async (span, text, typingDelay) => {
    if (typingDelay === 0)
      return span.innerHTML += text;
    return new Promise((resolve, reject) => {
      let charPosition = 0;
      const delay = () => {
        setTimeout(() => {
          span.innerHTML += text.charAt(charPosition++);
          if (charPosition < text.length)
            delay();
          else
            resolve();
        }, this.determineDelayDuration(typingDelay));
      }
      delay();
    });
  }  

  // called before a nodes typing is started
  pauseTyping = async (duration) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, duration);
    });
  }

  // called after a nodes text has been typed out. Deletes specified number of characters
  // with a specified delay between each deletion
  backspaceTextNode = async (span, { count, delay }) => {
    return new Promise((resolve, reject) => {
      let charPosition = count || span.innerText.length;
      const delayTyping = () => {
        setTimeout(() => {
          span.innerHTML = span.innerHTML.slice(0, -1);
          if (--charPosition)
            delayTyping();
          else
            resolve();
        }, delay ? (Array.isArray(delay) ? this.determineDelayDuration([delay[0], delay[0]]) : delay) : 100);
      }
      delayTyping();
    });
  }
  
  start = () => this.nextNode()
  
  // runs the operation of the next specified node
  nextNode = async () => {
    const { text, html, clear, pause, backspace, typingDelay, jumpBack, style } = this.nodes[this.index];
    
    if (pause)
      await this.pauseTyping(pause);

    if (clear)
      this.content.innerHTML = '';

    const span = this.appendNewSpanToContent(style);
    
    if (style)
      span.style = style

    if (text)
      await this.typeTextNode(span, text, typingDelay);

    if (html)
      span.innerHTML += html;
    
    if (backspace) {
      if (backspace.pause)
        await this.pauseTyping(backspace.pause);
      await this.backspaceTextNode(span, backspace);
    }
    
    if (jumpBack) {
      this.index = this.index - jumpBack;
      return this.nextNode();
    }

    if (this.nodes.length - 1 > this.index) {
      this.index = this.index + 1;
      return this.nextNode();
    }

    if (this.options.repeat && this.options.repeat.count !== 0) {
      this.options.repeat.count--;
      this.index = 0;
      if (this.options.repeat.clear)
        this.content.innerHTML = '';
      this.nextNode();
    }

    if (this.options.onComplete) {
      this.options.onComplete();
    }
  }
}