I have gone off on my own little project here, but hopefully there is something to dig out of it.
Paul was setting at least some properties on the stylesheet as far I can tell. I have opted for just adding the variables to an inline style on each text line. I do want to look into Paulâs approach though, there might be a more elegant solution using setProperties.
Code
HTML
To make the pseudo :after cursors work the text spans need a parent element to attach that to.
I am also making use of datasets to add a pause before the typing starts on certain lines â just to add a bit of suspense 
<div class='container'>
<div class='typewriter cursor-active'>
<span class='typewriter-text' data-delay='2'>GREETINGS PROFESSOR FALKEN</span>
</div>
<br>
<div class='typewriter'>
<span class='typewriter-text' data-delay='2'>HELLO</span>
</div>
<div class='typewriter'>
<span class='typewriter-text'>A STRANGE GAME</span>
</div>
<div class='typewriter'>
<span class='typewriter-text'>THE ONLY WINNING MOVE IS</span>
</div>
<div class='typewriter'>
<span class='typewriter-text'>NOT TO PLAY.</span>
</div>
<br>
<div class='typewriter'>
<span class='typewriter-text' data-delay='4'>HOW ABOUT A NICE GAME OF CHESS?</span>
</div>
</div>
CSS
The variables will be populated from the inline style properties, which are added in Javascript
e.g.
<span
class="typewriter-text"
style="--char-count: 24; --line-duration: 3s; --line-delay: 9.625s; --line-width: 24ch; animation-play-state: running;"
>THE ONLY WINNING MOVE IS</span>
.typewriter {
display: flex;
align-items: center;
.typewriter-text {
display: inline-flex;
width: 0;
overflow: hidden;
white-space: nowrap;
line-height: 1.2;
animation:
typing
var(--line-duration)
steps(var(--char-count))
forwards;
animation-delay: var(--line-delay);
animation-play-state: paused;
}
&.cursor-active:after {
content: '_';
margin-bottom: -2px;
align-self: end;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@keyframes typing {
to {
width: var(--line-width);
}
}
}
JS
I have commented the code, but if there are any questions, do feel free to ask.
const ANIM_CONFIG = {
speed: 8
}
// Helper functions - not necessary, but I think help for more readable code
function addClass(elem, className = '') {
elem.classList.add(className)
}
function removeClass(elem, className = '') {
elem.classList.remove(className)
}
function onAnimationStart(elem, fn) {
elem.addEventListener('animationstart', fn);
}
function onAnimationEnd(elem, fn) {
elem.addEventListener('animationend', fn);
}
/**
* Adds inline animation properties to the text element.
* @param {HTMLElement} textElem - The text element to add the properties to.
* @param {number} initDelay - line's delay in seconds before animation starts.
* @param {number} sumDelays - sum of all previous lines delays.
* @returns {number} duration - the duration time of the animation.
*/
function setlineProps(textElem, initDelay=0, sumDelays=0, config=ANIM_CONFIG) {
const charCount = textElem.textContent.length;
const duration = charCount / config.speed;
textElem.style = `
--char-count: ${charCount};
--line-duration: ${duration}s;
--line-delay: ${initDelay + sumDelays}s;
--line-width: ${charCount}ch;
animation-play-state: running
`;
// to be added to previous delays
return initDelay + duration;
}
window.addEventListener('DOMContentLoaded', () => {
const lines = document.querySelectorAll('.typewriter');
let sumDelays = 0;
for (const [i, line] of lines.entries()) {
const text = line.querySelector('.typewriter-text');
const initDelay = Number(text.dataset.delay || 0);
// setLineProps returns initDelay + duration
// this is added to previous delays
sumDelays += (setlineProps(text, initDelay, sumDelays));
// add cursor to new line and remove from previous line
onAnimationStart(text, () => {
// remove cursor from previous line if there is one
if (i > 0)
removeClass(lines[i-1], 'cursor-active');
// add cursor to new line
addClass(line, 'cursor-active');
});
}
})
Full example here
https://codepen.io/rpg2019/pen/azbXdeo
My css is a bit limited. It would be nice to do away with a lot of this JS and do something clever in CSS instead.
I would say these are good exercises to do. A bit frustrating at times, but thatâs how you learn 
edit: One thing I will share, donât round up numbers when setting durations, delays etc.
e.g. after a calculation like duration = charCount / config.speed
donât then try and round to 2 decimal places like I did.
--line-duration: ${duration.toFixed(2)}s;
instead just pass in as is
--line-duration: ${duration}s;
Albeit milliseconds difference the âeâ on âgameâ wouldnât show up if I added an initial delay of 1 or 2 seconds. It took quite a bit of debugging to try and figure out why I only had the issue with that particular line.