Styling a multi cursor Typewriter tutorial

1. Introduction

We are going to style a Typewriter animation with a multiple cursors, and selections.

The biggest difference between supporting multiple cursors and single cursors, is the fact that the cursor is perhaps not always at the end of a sentence. We therefore need to do a little bit more work to get the animation going.

To get started with the tutorial open and fork this sandbox. By using the sandbox you do not have to worry to much about setting up an entire development environment.

2. Files overview

In the sandbox you will see a couple of files, but these are the ones we are interested in:

  1. index.html contains the HTML for the two typewriters. It includes "uiloos" via the UNPKG cdn.
  2. main.js it contains a preconfigured multi cursor typewriter which is not working yet, and is unstyled.
  3. main.css a CSS file which is loaded in the index.html. We will style the multi cursor animation here.

3. Laying the foundation

In the "index.html" file you will see a div with the id typewriter. This element will contain our entire animation.

When using multiple cursors the best approach is to iterate over each position in the Typewriter, and render each position separately.

So what are "positions" exactly? Take the string "foo" it has 3 positions: "f", "o" and "o". Basically every character in the Typewriter's text is a position.

Each position is represented by a TypewriterPosition, it contains the "character" at the position, the cursors at the position, and which cursors are selecting the position.

So our goal is to loop over these positions, render the character, cursors and wether or not the position is selected. We also want to render the name of the cursor so we know which "user" is typing what.

Lets begin by rendering the characters:

To do this open the "main.js" file and change the "subscriber" function to:

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

  for (const position of typewriter) {
    const letterEl = document.createElement("span");
    letterEl.className = "letter";
    letterEl.textContent = position.character;

    typewriterEl.append(letterEl);
  }
}

What happens here is that our "subscriber" now listens to each event of the Typewriter.

For each event it clears the typewriter element by setting the innerHTML to "".

It then iterates over each position by iterating over the typewriter itself, and adding a span with the CSS class letter.

You should see the following animation, which only shows textual changes:

A simple basic animation showing a text being typed in letter by letter

4. Styling the typewriter

Now that we can see the "characters" / "letters" appearing it will be nice to show the cursors that typed them:

Open the file name named "main.js" and and update the "subscriber" function:

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

  for (const position of typewriter) {
    // By reversing we prioritize the last cursor by rendering it on top. 
    for (const cursor of position.cursors.reverse()) {
      const cursorEl = document.createElement("span");
      cursorEl.classList.add("cursor");

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

      typewriterEl.append(cursorEl);
    }

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

    typewriterEl.append(letterEl);
  }
}

Now each cursor is rendered at the correct position. Each cursor is represented by a span with the CSS class cursor.

Also note that cursors are rendered before the letters! This is how a text editor would render this as well.

To style the cursors and typewriter a little, open the file name named "main.css" and add the following:

#typewriter {
  position: relative;
  font-size: 32px;
}

#typewriter .cursor {
  position: absolute;
  width: 2px;
  height: 32px;
  margin-left: -1px;
  margin-top: 4px;
  background-color: black;
}

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

@keyframes blink {
  from,
  to {
    background-color: transparent;
  }
  50% {
    background-color: black;
  }
}

First look at the resulting animation before we continue and unpack the CSS:

A styled typewriter animation showing multiple cursors

The trickiest bit in the CSS is the way we are positioning the cursor so it does not affect the text.

To understand the problem we are trying to solve temporarily change the #typewriter .cursor rule to:

#typewriter .cursor {
  display: inline-block;
  width: 2px;
  height: 32px;
  background-color: black;
}

As you can see animation becomes glitchy because the cursor now takes up space in between the letters. The letters also move around when the cursor moves. This is not how a cursor should behave.

Now change the #typewriter .cursor back to what it was.

So how did we get the cursor not to affect the text:

  1. We gave the .cursor a position: absolute; So it goes outside of the normal flow. This way it does not take up any "space" from the letters.
  2. By making the #typewriter's position relative the cursor becomes absolute relative to the #typewriter instead of the window.
  3. By setting the width of the .cursor we make it visible.
  4. By setting the height of the .cursor to the font-size we make sure the cursors has the right height.
  5. By setting the margins of the .cursor we move it relatively to where it is rendered normally, which looks better.

5. Adding info boxes

Currently there is no way of seeing which cursor / user is typing.

A cursor can contain data, in which you can put anything you want.

In our case in the config variable you can see that we set a name and a color.

Lets add an info box above each cursor, showing the name of the user.

In "main.js" in change the "subscriber" to:

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

  for (const position of typewriter) {
    for (const cursor of position.cursors.reverse()) {
      const cursorEl = document.createElement("span");
      cursorEl.classList.add("cursor");

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

      typewriterEl.append(cursorEl);

      const infoEl = document.createElement("span");
      infoEl.className = "info";
      infoEl.textContent = cursor.data.name;

      typewriterEl.append(infoEl);
    }

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

    typewriterEl.append(letterEl);
  }
}

This will add a info span element after each cursor, containing the name of the cursor.

Next in the "main.css" give a little style to the info box by appending:

#typewriter .info {
  position: absolute;
  margin-top: -20px;
  margin-left: -1px;
  padding: 4px;
  font-family: sans-serif;
  font-weight: bold;
  font-size: 12px;
  background-color: black;
  color: white;
}

This lands us at the following animation:

A styled typewriter animation showing multiple cursors and info boxes in black and white

6. A splash of color

It has been a pretty black and white affair so far, lets add a splash of color.

The technique I've chosen is to set CSS variables from inside of JavaScript. Alternatively you could also set the "style" directly.

The color comes from the data of the cursor. Lets start by setting some CSS variables from within "main.js".

Replace the "subscriber"'s cursor loop with the following code:

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("blinking");
  }

  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);
}

In the "main.css" change the background-color of the info span, cursor span, and blink animation:

#typewriter .cursor {
  position: absolute;
  /* Abbreviated for brevity */
  background-color: var(--cursor-color);
}

@keyframes blink {
  from,
  to {
    background-color: transparent;
  }
  50% {
    background-color: var(--cursor-color);
  }
}

#typewriter .info {
  position: absolute;
  /* Abbreviated for brevity */
  background-color: var(--cursor-color);
  color: white;
}

You should now see some color:

A styled typewriter animation showing multiple cursors and info boxes in color

7. Handling selections

The last thing on our list is handling selections, when a cursor selects text with the mouse the background of the letters should become the color of the cursor.

We achieve this by giving a selected the letter span, CSS variable for the background color. We will call this CSS variable:
--background-color

Change the "subscriber" in "main.js" to:

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

  for (const position of typewriter) {
    for (const cursor of position.cursors.reverse()) {
      // Abbreviated for brevity still the same
    }

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

    for (const cursor of position.selected.reverse()) {
      // If there are multiple cursors, the last one will win.
      const color = cursor.data.color;
      letterEl.style.setProperty("--background-color", color + "30"); // 30 = opacity
    }

    typewriterEl.append(letterEl);
  }
}

Then append the following to "main.css":

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

This results in the final animation in which the text "three" gets selected:

A  styled typewriter animation showing multiple cursors and info boxes in color, and which also shows selections.

Protip: for selections it looks nice if the cursor selects it for a while, this gives the user (viewing the animation) time to see the "selection".

To achieve this I set the delay to 2 seconds in the "config" in the "main.js" in the action that comes after the "mouse" action.

For reference here is the full code.

8. Further reading

  1. Read through the API of the Typewriter.
  2. Only want one cursors see the tutorial for a single cursor animation instead.
  3. When using vanilla JavaScript you must handle DOM manipulation yourself, contrast this with the examples that use a framework.
Usage with Frameworks
Learn how to use the Typewriter in combination with frameworks such as Angular and Vue.