Weekly Dev Project #002 — Typing Trainer

Jakub Korch
Nerd For Tech
Published in
15 min readDec 12, 2023

This article is more or less transcript of my free YouTube series about making simple web development projects. If you prefer watching over reading, feel free to visit my channel “Dev Newbs”.

Hey everyone, welcome back to Dew Newbs! My name is Jacob and today, we’re diving into a practical and hands-on project that’s not only fun to build, but can also significantly enhance your typing skills. We’re going to create a custom Typing Trainer using HTML, CSS, and JavaScript.

“Why this project?” you might ask. “What’s the motivation behind creating a typing trainer?” The answer is simple: enhancing your typing speed and accuracy isn’t just a valuable skill in our digital era, but it can also significantly enhance your overall productivity. Whether you’re a student, a working professional, or someone eager to elevate their keyboard skills, this project is tailor-made for you.

If you’re ready to level up your typing game, hit that subscribe button, give this video a thumbs up, and let’s jump into building our very own Typing Trainer from scratch. Let’s get started!

Setting Up the Project Structure

First thing we will do is open your favorite editor. Mine is Visual Studio Code. I will create a new folder which will contain all of the code and data we need to run our application.

We will need multiple files:

  • index.html,
  • styles.css,
  • script.js,
  • quotes.json

HTML

I can generate boilerplate code for my index.html in Visual Studio Code easily by typing html:5 and pressing enter. This generates the minimal viable HTML code that is valid.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

</body>
</html>

Now I need to include external dependencies, like a fonts from Google Fonts. I have decided to use a font called Roboto Mono, because I believe it fits nicely with our intent.

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300;400;500;600;700&display=swap" rel="stylesheet">

I also need to link to an external stylesheet document where all the styling will take place.

<link rel="stylesheet" href="styles.css">

That’s all for the head section of the HTML. Now let’s put some content into the body.

We will need to show the progress of the training like how many mistakes the trainee made, what is the current progress and also how much time it took so far. We can identify this division as a “status-bar”. It will contain three additional divs, each for a separate metric.

   <div id="status-bar">
<div>
<p>Mistakes made: <span id="mistakes-made">0</span></p>
</div>
<div>
<p>Current progress: <span id="current-progress">0 / 0 </span></p>
</div>
<div>
<p>Elapsed time: <span id="elapsed-time">00:00:00</span></p>
</div>
</div>

Next part will be a container that will hold the text for the training itself. Let’s simply call it “container”.

<div id="container"></div>

The last part of HTML will be a modal window which shows the overall results for given training text, once finished. It should contain a header with a nice clear message. Also the text with all the information about metrics that were measured and lastly a call to action button which restarts the application.

 <div id="result-modal">
<h1>You have finished!</h1>
<p>You wrote <strong><span id="result-characters">0</span> characters</strong> in <strong><span id="result-time">0</span> seconds</strong>, which translates to <strong><span id="result-tempo">0</span> characters per minute</strong>. You also made <strong><span id="result-typos">0</span> mistakes</strong>.</p>
<div class="button" id="try-again">TRY AGAIN</button>
</div>

Besides this HTML code, we need to place a script tag that references the external dependency — the file with our JavaScript code.

<script src="script.js" type="application/javascript"></script>

That’s HTML out of our way. Let’s handle CSS styling next.

CSS

As usual, we need to start from the top, so let’s style the body tag.

body {
width: 100vw;
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
border: none;
display: flex;
align-items: center;
justify-content: center;
background-color: #eee;
font-family: 'Roboto Mono', monospace;
}

We do not want any overflows, so we set it to hidden and width and height to 100%. To make sure there is no scrollbar, we set margins and paddings to zero and border to none.

Everything should be placed in the middle of the page, so we will go with flex display and align-items and justify-content with the value center. The background can be a little more gray, because absolute white looks weird to me. The font family will be Roboto Mono.

Okay, some changes are happening already after I saved the changes. Let’s now style our “status-bar”.

#status-bar {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100px;
padding: 12px;
font-size: 2vh;
color: #222;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: row;
}

We identify it by its ID, because there should only be one in the whole app. The position should be fixed, specifically to the top of the viewport. Since it will take the whole width of the viewport, the left position should be equal to zero just like top. The height will be a fixed number — 100px should be just the right amount of space. Besides that, let’s give it some padding of 12px and set size relatively to the size of viewport height as 2vh, which is equal to 2% of total viewport height. Lastly let’s set the color of text to an off-black color using shorthand value #222 and give the status bar flex display. In this case, we spread the content horizontally using justify-content space-around. The direction the elements should flow is row.

Now, when the status bar looks presentable, let’s focus on the modal window with results.

#result-modal {
position: fixed;
width: 400px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 2px;
box-sizing: border-box;
padding: 24px 48px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}

The modal windows with the results should ignore the flex display configuration of the body element. It can be achieved with position set to fixed just like with the status bar. On top of that, let’s give it a fixed width of 400px. We need to differentiate it from the background, so we set the color of the modal window to bright white and also put a border that further separates it from the background content. We round the border a little, so it looks more smooth. Also padding makes it a little easier on the eyes. We want the 400px to be the final width, so we set box-sizing to value border-box which guarantees that. Display flex and its usual followers justify-content and align-items with value center put everything nicely in the middle of the modal window. Direction of the element flowing is now column, so they are organized one above the other. Text-align ensures that text is nicely centered.

Everything looks good, with the exception of our call to action button. It’s not styled and it does not even allow clicking. We need to do something about that.

.button {
padding: 6px 24px;
color: #fff;
font-size: 18px;
font-weight: 200;
background-color: #333;
border: 6px solid #333;
border-radius: 2px;
margin: 24px 0px 0px 0px;
cursor: pointer;
user-select: none;
transition: 0.25s all;
}

.button:hover {
background-color: #fff;
color: #333;
font-weight: 600;
transition: 0.25s all;
}

First of all, we might have more similar buttons, so let’s target it by the class called “button”. Some padding to make it a little bigger always helps. We set the color of the button text to white and change the font size to a larger one with a value of 18px. Color of the button can be slightly lighter black with value #333. Let’s also define the border, because we will animate the background, so we want our button to be defined exactly by the borders. Also let’s put margins so there is some white space around the button itself. By setting the cursor to a pointer we show the little hand when hovering over the button. The user-select property makes sure the text of the button is not selectable. Lastly let’s give it a transition with quarter second duration on all changes, which will allow for a smooth hover animation.

The hover properties of button follow the button properties. We set the lighter background and darker text color with little bolder text. The transition will take care of the rest.

OK, so far so good. Everything looks great. So now it is time to style the text that will be used for training. For that to happen, we first need to hide the modal window which is in the way. That is quite simple.

Let’s give it a display set to none, until the logic in the JavaScript code decides something else.

display: none;

Now, when the modal is invisible, we need to put some text into the container. In order to do that, we need some texts to work with. I have downloaded some quotes from GitHub in the form of a JSON file and copied it into my file called quotes.json. We will pick some random quote from there and manually enter it into the container, so we can see how the styling will work.

<p>Whatever the mind of man can conceive and believe, it can achieve.</p>

The styling of the container should be pretty simple, so let’s get it done.

#container {
width: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2vh;
color: #222;
text-align: center;
line-height: 1.5;
letter-spacing: 5px;
}

Again, there is just one container in our app and it will have the ID “container”. We set its width to 50% of the viewport, because we want to have some white space around. The display mode will be flex with everything centered in the middle. The font size will be exactly as the one in the top bar — equal to 2% of total height of viewport, so 2vh. I decided on the little lighter black with value #222. Again, just in case, let’s center everything in the center using text-align and set the line-height to one and half of font size. Also, let’s make a little bit more space horizontally between the letters, so it is easier to track their status.

If we had a better look at the finished application, we would see that each letter is a separate span element containing one character and possibly a class depending whether it is a character which is currently being typed or it was correctly or incorrectly typed before.

<span>H</span><span>e</span><span>l</span><span>l</span><span>o</span><span> </span><span>W</span><span>o</span><span>r</span><span>l</span><span>d</span><span>!</span>

Let’s save and see what we see. Perfecto! Let’s now format it a bit.

The class current will be given to the next character that needs to be typed. It will have white color and very complex text-shadow which honestly, just copy-paste it from me, unless you want to use your own.

.current {
color: #fff;
text-shadow: 0 1px 0 #ccc,
0 2px 0 #c9c9c9,
0 3px 0 #bbb,
0 4px 0 #b9b9b9,
0 5px 0 #aaa,
0 6px 1px rgba(0,0,0,.1),
0 0 5px rgba(0,0,0,.1),
0 1px 3px rgba(0,0,0,.3),
0 3px 5px rgba(0,0,0,.2),
0 5px 10px rgba(0,0,0,.25),
0 10px 10px rgba(0,0,0,.2),
0 20px 20px rgba(0,0,0,.15);
}

In the similar manner, let’s create classes “correct” and “wrong” respectively and give it a red or green color based on the result.

.correct {
color: #1db550;
}

.wrong {
color:#b51d20;
}

Now when we save the changes, we will see that…. well nothing changed. And that is because? Because we did not really give any span in the container any of these classes. So let’s do exactly that right now.

<span class="correct">H</span><span class="correct">e</span><span class="wrong">l</span><span class="wrong">l</span><span class="current">o</span><span> </span><span>W</span><span>o</span><span>r</span><span>l</span><span>d</span><span>!</span>

After we gave some of the span elements classes, it now looks almost like a working app. But the metrics are not updated, time is not running and there is no interaction really. So we need to start writing the JavaScript code and enable the interactivity. Let’s get to it!

JavaScript

First of all, let’s delete the contents of the “container”, so we can generate them dynamically. Much better. Now let’s open the file script.js and start writing our business logic.

When we look at our HTML code, many elements have IDs so that we can reference them and then populate certain elements with values we calculate dynamically. Let’s create variables that reference all these elements.

let container = document.getElementById("container")
let mistakesSpan = document.getElementById("mistakes-made")
let progressSpan = document.getElementById("current-progress")
let timeSpan = document.getElementById("elapsed-time")

let resultModal = document.getElementById("result-modal")
let resultChars = document.getElementById("result-characters")
let resultTime = document.getElementById("result-time")
let resultTempo = document.getElementById("result-tempo")
let resultTypos = document.getElementById("result-typos")
let buttonRestart = document.getElementById("try-again")

Somewhere down the road, we will need all these references. We need to reference the container itself, so we can populate it with our quote texts. We need elements in status-bar to update the number of mistakes made, the progress and elapsed time. In the same manner, we need to target the modal window with the results, so we can share the final values of different metrics there.

Another thing to do is to create all the variables that will hold dynamically generated text. Firstly an array that will hold all the texts from the JSON file.

let textsToTrain = []

Let’s call it textsToTrain and define it as an empty array. Following this, we need an asynchronous function that will read the contents of the file. But first, let’s see the JSON file itself and let’s describe its structure.

JSON FILE

The file itself is one JSON object that contains a single property called “quotes”. This property contains an array of objects, where each object is a quote with two properties — quote and author. We do not really care about the author.

We create an asynchronous function “fetchTexts” that will load all the quotes from the JSON file and put it into our array “textsToTrain”.

const fetchTexts = async () => {
const response = await fetch('./quotes.json')
const quotes = await response.json()
const texts = quotes.quotes.map(x => x.quote)
textsToTrain = texts
}

I am using an arrow function to declare the function and a word async to signify that it is an asynchronously running function. The reference to this function is passed to a constant with name “fetchTexts”.

The function itself uses fetch in tandem with await, to get the contents of the file into variable response. Then again using await it is converted into a JSON object that can be further parsed using array method map() to get only the quote part of the quotes objects. Finally I store the extracted contents into the array variable for use later.

Next, we need to declare all the variables that will hold dynamically changing values.

let randomText
let paragraph
let mistakes
let spans
let position
let progress
let now
let elapsedTime

Variable “randomText” will hold on quote at a time, “paragraph” will reference to newly created paragraph with text, “spans” will reference all the spans inside paragraph, “mistakes” will hold number of incorrectly typed characters, “position” will point towards the current position within text, “progress” will be a string containing position and total length of the used quote text, “now” is there for the current time and “elapsedTime” is a reference to the setInterval function that will be updated every second and will set time in the status-bar.

The following step is to create a function that will initialize the whole application. It should randomly pick one of the quotes for training, then generate the contents of the paragraph as individual span elements — each holding exactly one character — and inject them into the HTML content of the container element. If there was any previous content in the container element, this should be removed before populating it with the new one. All the variables tracking progress should be reset to its initial values and user interface — specifically the status bar should be reset as well with these values.

const initScript = async () => {
randomText = textsToTrain[Math.floor(Math.random() * textsToTrain.length)]
paragraph = document.createElement("p")

for(let c of randomText){
let span = document.createElement("span")
span.textContent = c
paragraph.appendChild(span)
}

// remove previous Paragraphs
while (container.firstChild) {
container.removeChild(container.firstChild);
}

container.appendChild(paragraph)

spans = document.querySelectorAll("#container > p > span")
position = 0
spans[position].classList.add("current")

// init MISTAKES, PROGRESS, TIME
mistakes = 0;
mistakesSpan.textContent = mistakes

progress = position + " / " + (randomText.length - 1)
progressSpan.textContent = progress

now = new Date()

elapsedTime = setInterval(function(){
timeDifference = new Date(new Date().getTime() - now.getTime())

timeSpan.textContent = ("0" + timeDifference.getUTCHours()).slice(-2) + ":" + ("0" + timeDifference.getUTCMinutes()).slice(-2) + ":" + ("0" + timeDifference.getUTCSeconds()).slice(-2)
}, 1000)
}

As was said before, we pick random quote to be the training text. Then we create a new paragraph element. We loop over the characters in a randomly picked quote and create span elements that are subsequently added to the paragraph element as its children.

After that, we make sure that the container element is empty by removing the first child element as long as there is some available. Once this is done, we populate the container with our newly generated content.

The variable “spans” is set to reference these newly appended spans so we can use it later. Position variable is set to its initial value — which is zero.

We add the class “current” to the first character, so the user knows which character needs to be pressed on the keyboard next.

All the other metric tracking variables like mistakes and progress are set and also their representation on status-bar is reset to initial values.

Lastly the setInterval function is started. This function will check the time every second and will update the status-bar’s elapsed time accordingly.

Once we have this piece of code done, we need to actually call it at the initial load of the page.

// invoke loading of the file and initialize app once
(async () => {
await fetchTexts()
await initScript()
})()

Since both functions — fetchTexts() and initScript() are asynchronous, we can not call them in a global scope using await. Therefore we create a self-invoking async function, which contains calls to both our functions with the await keyword. This will make sure that these activities happen only once, when the page is loaded.

Cool! After saving the changes, we can see a randomly chosen quote set in the middle of the screen and the first character is highlighted using CSS class current. However, there is still no interactivity. Even if you press the keyboard, nothing happens. That’s because we need to create an event listener for action “keypress” and handle the pressed key there. So let’s do that now.

document.addEventListener("keypress", function(event){
if(position >= randomText.length){
return
}

if((randomText[position] == " " && spans[position].textContent == "_" && event.key == " ") || (spans[position].textContent == event.key)){
spans[position].classList.add("correct")
spans[position].classList.remove("current")
}
else {
spans[position].classList.add("wrong")
spans[position].classList.remove("current")
mistakes++;
mistakesSpan.textContent = mistakes
}

if(spans[position].textContent == "_"){
spans[position].textContent = " "
}
position++;

if(position == randomText.length){
clearInterval(elapsedTime)

// update MODAL with results
resultChars.textContent = randomText.length - 1
resultTime.textContent = Math.round(timeDifference.getTime() / 1000)
resultTempo.textContent = Math.round((randomText.length - 1) / (timeDifference.getTime() / 1000) * 60)
resultTypos.textContent = mistakes
resultModal.style.display = "flex"

return
}

spans[position].classList.add("current")
if(spans[position].textContent == " "){
spans[position].textContent = "_"
}

progress = position + " / " + (randomText.length - 1)
progressSpan.textContent = progress
})

Firstly we create a condition to ignore any keys pressed once the whole text has been finished. We can do that simply by checking position and if it’s greater or equal to the length of the text, we are done with the text and can just return the event listener.

Secondly, we need to check the current character with the value of the key that was pressed. We can encounter two situations — either character is empty space or not. If the character is an empty space, the situation is a little bit special. In order to make the empty space visible when it is the current character, the app adds the character “underscore” to the span. So we need to check whether span contains character “underscore” while at the same time the variable with randomly chosen text contains “empty space” at the given position and lastly we need to check if the key pressed was indeed for “empty space”.

In case the character is something else, we simply check the key pressed and value in the span element.

If the conditions are met, we add class “correct” and if not, we add class “wrong” and in either case we remove class “current” from the given span.

After that we need to increment the variable position which holds the current position.

In the second half of the code, we check if the end of the training text was reached. If so, we reset the elapsedTime interval and update the modal window content with the resulting values and then return from the event listener code.

If the end has not yet been reached, we add the class “current” to the next character in line. If this character is “empty space”, we also set the text of the span to the character “underscore”.

We then update the variable for progress and its interface representation in a “status-bar”.

Once you save your code and reach the end of the training text, you will see that the modal window appears with the metrics populated. But the button for restart does not work. So the last thing we need to do is to create an event listener for this as well. Since we have a variable that references the button, we can use that.

buttonRestart.addEventListener("click", function(event){
resultModal.style.display = "none"
initScript()
})

This last missing event listener waits for click action on a specific element — the try again button and when clicked, it hides the modal window and executes the initScript().

Now, the Typing trainer is finally complete. After finishing training, the modal window appears showing the results of our typing attempt. Once you click on a button “Try Again”, the new training will commence.

If you would like to see the whole code in one place, feel free to check it in my public GitHub repository here:

https://github.com/j-cup/weekly-dev-project/tree/main/002-typing-trainer

Thank you so much for joining me on this tutorial journey to create a simple but effective Typing trainer. I hope you found the content interesting and that it helps you in your coding endeavors.

If you have any questions or suggestions, feel free to leave them in the comments below. Don’t forget to like, subscribe, and hit the notification bell to stay updated on future tutorials.

Happy coding, and until next time, goodbye and keep typing away!

--

--

Nerd For Tech
Nerd For Tech

Published in Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.

Jakub Korch
Jakub Korch

Written by Jakub Korch

Web enthusiast, programmer, husband and a father. Wannabe entrepreneur. You name it.

No responses yet