thumbnail

Typing Speed Tester Using HTML, CSS and JavaScript

Introduction :

The Typing Speed Test Game is a web-based application created using HTML, CSS, and JavaScript. The purpose of this game is to assess and improve the user’s typing speed and accuracy. The game presents the user with a random set of words, and the user needs to type them as quickly and accurately as possible within a specified time limit. The application features various options such as selecting the time duration (30s or 60s) and difficulty level (beginner or pro).The primary objective of this Typing Speed Test Game is to offer users a platform to refine their typing skills while also providing an accurate measure of their typing speed and word-per-minute (WPM) count. The interface is visually appealing, featuring a clean and intuitive design, making it accessible to a wide range of users.

The project’s structure is organized into different sections, each serving a specific purpose. The HTML file defines the overall layout and structure of the game, including elements such as the timer, word display area, input field, and restart button. The Bootstrap library is leveraged for responsive and visually consistent styling. JavaScript plays a crucial role in implementing the game’s logic and functionality. The script manages various aspects, such as tracking user input, updating the timer, handling word validation, calculating WPM, and dynamically generating and displaying random sets of words. It also incorporates event listeners to respond to user interactions, allowing for a seamless and interactive experience.

In summary, the Typing Speed Test Game is a well-crafted project that combines HTML, CSS, and JavaScript to create an engaging and educational typing experience. Its user-friendly interface, customizable options, and real-time feedback make it an effective tool for users looking to enhance their typing proficiency.

Explanation :

HTML Structure:

  • The HTML file starts with the document type declaration and includes meta tags for character set and viewport settings.
  • The title of the page is set to “Typing Test.”
  • Bootstrap 4 and Font Awesome libraries are included for styling and icons.
  • The page includes a container-fluid with various div elements for different sections of the game, such as top, stats, text display, input area, and restart button.
  • Links to external CSS and JavaScript files are included at the end of the body.

JavaScript Logic:

  • Variable Declarations:

    • Several variables are declared to store elements and game-related data, such as wordNo, wordsSubmitted, wordsCorrect, timer, flag, factor, seconds, and difficulty.
  • Event Listeners:

    • The script listens for input events on the text input area. When a user starts typing, it checks for whitespace to determine if a word is complete.
    • Event listeners are also set for time and difficulty selection buttons.
    • The restart button has an event listener to reset the game.
  • Time and Difficulty Selection:

    • The user can choose between 30s and 60s time limits and beginner or pro difficulty levels.
    • Functions for time and difficulty selection update relevant variables and UI elements.
  • Game Initialization:

    • The displayTest function is called initially to populate the textDisplay with a set of random words based on the selected difficulty.
  • Typing Logic:

    • On user input, the script checks if the character entered is a whitespace. If true, it calls the checkWord function; otherwise, it calls the currentWord function.
    • The currentWord function checks if the user is entering the correct characters for the current word.
  • Restart Functionality:

    • The restartBtn click event triggers a function that resets various game-related variables, clears the input area, and starts a new game.
  • Timer Functionality:

    • The timeStart function initiates a countdown timer based on the selected time limit. It also hides certain UI elements during the typing session.
    • When the timer reaches zero, the timeOver function is called, disabling the input area and displaying the user’s score.
  • Score Display:

    • The displayScore function calculates and displays the user’s accuracy percentage and words per minute (WPM).
  • Word Checking and Coloring:

    • Functions like colorSpan, checkWord, and currentWord are responsible for checking and coloring the words based on user input.
  • Word Display:

    • The displayTest function generates a set of random words based on the selected difficulty level and displays them in the textDisplay area.
Purpose of Functions:
  • timeStart: Initiates the countdown timer and hides unnecessary UI elements.
  • timeOver: Disables the input area and triggers the score display.
  • limitVisible and limitInvisible: Control the visibility of time and difficulty selection buttons.
  • displayScore: Calculates and displays the user’s accuracy percentage and WPM.
  • colorSpan, checkWord, and currentWord: Manage the coloring and validation of user input against the displayed words.
  • displayTest: Generates a set of random words based on the selected difficulty level and displays them on the screen.

Overall, the JavaScript code efficiently handles the game’s logic, user input, timer functionality, and score calculation, providing a smooth and interactive typing speed test experience.

Source Code :

HTML (index.html)

				
					<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="../../logo.svg" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="index.js" type="module"></script> <link rel="stylesheet" href="style.css" />
</head>

<body>
  <div class="container text-center">
    <h4>Start typing to measure your speed.</h4>

    <p>
      You can click on virtual keyboard as well to type. It contains only small letters and
      numbers
    </p>

    <div class="sentences-display"></div>
    <div class="keyboard"></div>
    <section><span class="time-display"></span>&nbsp;seconds</section>
    <button type="button" class="btn btn-primary reset">Reset</button>
  </div> <script data-no-optimize="1">!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).LazyLoad=e()}(this,function(){"use strict";function e(){return(e=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,a=arguments[e];for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(t[n]=a[n])}return t}).apply(this,arguments)}function i(t){return e({},it,t)}function o(t,e){var n,a="LazyLoad::Initialized",i=new t(e);try{n=new CustomEvent(a,{detail:{instance:i}})}catch(t){(n=document.createEvent("CustomEvent")).initCustomEvent(a,!1,!1,{instance:i})}window.dispatchEvent(n)}function l(t,e){return t.getAttribute(gt+e)}function c(t){return l(t,bt)}function s(t,e){return function(t,e,n){e=gt+e;null!==n?t.setAttribute(e,n):t.removeAttribute(e)}(t,bt,e)}function r(t){return s(t,null),0}function u(t){return null===c(t)}function d(t){return c(t)===vt}function f(t,e,n,a){t&&(void 0===a?void 0===n?t(e):t(e,n):t(e,n,a))}function _(t,e){nt?t.classList.add(e):t.className+=(t.className?" ":"")+e}function v(t,e){nt?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\s+)"+e+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")}function g(t){return t.llTempImage}function b(t,e){!e||(e=e._observer)&&e.unobserve(t)}function p(t,e){t&&(t.loadingCount+=e)}function h(t,e){t&&(t.toLoadCount=e)}function n(t){for(var e,n=[],a=0;e=t.children[a];a+=1)"SOURCE"===e.tagName&&n.push(e);return n}function m(t,e){(t=t.parentNode)&&"PICTURE"===t.tagName&&n(t).forEach(e)}function a(t,e){n(t).forEach(e)}function E(t){return!!t[st]}function I(t){return t[st]}function y(t){return delete t[st]}function A(e,t){var n;E(e)||(n={},t.forEach(function(t){n[t]=e.getAttribute(t)}),e[st]=n)}function k(a,t){var i;E(a)&&(i=I(a),t.forEach(function(t){var e,n;e=a,(t=i[n=t])?e.setAttribute(n,t):e.removeAttribute(n)}))}function L(t,e,n){_(t,e.class_loading),s(t,ut),n&&(p(n,1),f(e.callback_loading,t,n))}function w(t,e,n){n&&t.setAttribute(e,n)}function x(t,e){w(t,ct,l(t,e.data_sizes)),w(t,rt,l(t,e.data_srcset)),w(t,ot,l(t,e.data_src))}function O(t,e,n){var a=l(t,e.data_bg_multi),i=l(t,e.data_bg_multi_hidpi);(a=at&&i?i:a)&&(t.style.backgroundImage=a,n=n,_(t=t,(e=e).class_applied),s(t,ft),n&&(e.unobserve_completed&&b(t,e),f(e.callback_applied,t,n)))}function N(t,e){!e||0<e.loadingCount||0<e.toLoadCount||f(t.callback_finish,e)}function C(t,e,n){t.addEventListener(e,n),t.llEvLisnrs[e]=n}function M(t){return!!t.llEvLisnrs}function z(t){if(M(t)){var e,n,a=t.llEvLisnrs;for(e in a){var i=a[e];n=e,i=i,t.removeEventListener(n,i)}delete t.llEvLisnrs}}function R(t,e,n){var a;delete t.llTempImage,p(n,-1),(a=n)&&--a.toLoadCount,v(t,e.class_loading),e.unobserve_completed&&b(t,n)}function T(o,r,c){var l=g(o)||o;M(l)||function(t,e,n){M(t)||(t.llEvLisnrs={});var a="VIDEO"===t.tagName?"loadeddata":"load";C(t,a,e),C(t,"error",n)}(l,function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_loaded),s(e,dt),f(n.callback_loaded,e,a),i||N(n,a),z(l)},function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_error),s(e,_t),f(n.callback_error,e,a),i||N(n,a),z(l)})}function G(t,e,n){var a,i,o,r,c;t.llTempImage=document.createElement("IMG"),T(t,e,n),E(c=t)||(c[st]={backgroundImage:c.style.backgroundImage}),o=n,r=l(a=t,(i=e).data_bg),c=l(a,i.data_bg_hidpi),(r=at&&c?c:r)&&(a.style.backgroundImage='url("'.concat(r,'")'),g(a).setAttribute(ot,r),L(a,i,o)),O(t,e,n)}function D(t,e,n){var a;T(t,e,n),a=e,e=n,(t=It[(n=t).tagName])&&(t(n,a),L(n,a,e))}function V(t,e,n){var a;a=t,(-1<yt.indexOf(a.tagName)?D:G)(t,e,n)}function F(t,e,n){var a;t.setAttribute("loading","lazy"),T(t,e,n),a=e,(e=It[(n=t).tagName])&&e(n,a),s(t,vt)}function j(t){t.removeAttribute(ot),t.removeAttribute(rt),t.removeAttribute(ct)}function P(t){m(t,function(t){k(t,Et)}),k(t,Et)}function S(t){var e;(e=At[t.tagName])?e(t):E(e=t)&&(t=I(e),e.style.backgroundImage=t.backgroundImage)}function U(t,e){var n;S(t),n=e,u(e=t)||d(e)||(v(e,n.class_entered),v(e,n.class_exited),v(e,n.class_applied),v(e,n.class_loading),v(e,n.class_loaded),v(e,n.class_error)),r(t),y(t)}function $(t,e,n,a){var i;n.cancel_on_exit&&(c(t)!==ut||"IMG"===t.tagName&&(z(t),m(i=t,function(t){j(t)}),j(i),P(t),v(t,n.class_loading),p(a,-1),r(t),f(n.callback_cancel,t,e,a)))}function q(t,e,n,a){var i,o,r=(o=t,0<=pt.indexOf(c(o)));s(t,"entered"),_(t,n.class_entered),v(t,n.class_exited),i=t,o=a,n.unobserve_entered&&b(i,o),f(n.callback_enter,t,e,a),r||V(t,n,a)}function H(t){return t.use_native&&"loading"in HTMLImageElement.prototype}function B(t,i,o){t.forEach(function(t){return(a=t).isIntersecting||0<a.intersectionRatio?q(t.target,t,i,o):(e=t.target,n=t,a=i,t=o,void(u(e)||(_(e,a.class_exited),$(e,n,a,t),f(a.callback_exit,e,n,t))));var e,n,a})}function J(e,n){var t;et&&!H(e)&&(n._observer=new IntersectionObserver(function(t){B(t,e,n)},{root:(t=e).container===document?null:t.container,rootMargin:t.thresholds||t.threshold+"px"}))}function K(t){return Array.prototype.slice.call(t)}function Q(t){return t.container.querySelectorAll(t.elements_selector)}function W(t){return c(t)===_t}function X(t,e){return e=t||Q(e),K(e).filter(u)}function Y(e,t){var n;(n=Q(e),K(n).filter(W)).forEach(function(t){v(t,e.class_error),r(t)}),t.update()}function t(t,e){var n,a,t=i(t);this._settings=t,this.loadingCount=0,J(t,this),n=t,a=this,Z&&window.addEventListener("online",function(){Y(n,a)}),this.update(e)}var Z="undefined"!=typeof window,tt=Z&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),et=Z&&"IntersectionObserver"in window,nt=Z&&"classList"in document.createElement("p"),at=Z&&1<window.devicePixelRatio,it={elements_selector:".lazy",container:tt||Z?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_poster:"poster",class_applied:"applied",class_loading:"litespeed-loading",class_loaded:"litespeed-loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1},ot="src",rt="srcset",ct="sizes",lt="poster",st="llOriginalAttrs",ut="loading",dt="loaded",ft="applied",_t="error",vt="native",gt="data-",bt="ll-status",pt=[ut,dt,ft,_t],ht=[ot],mt=[ot,lt],Et=[ot,rt,ct],It={IMG:function(t,e){m(t,function(t){A(t,Et),x(t,e)}),A(t,Et),x(t,e)},IFRAME:function(t,e){A(t,ht),w(t,ot,l(t,e.data_src))},VIDEO:function(t,e){a(t,function(t){A(t,ht),w(t,ot,l(t,e.data_src))}),A(t,mt),w(t,lt,l(t,e.data_poster)),w(t,ot,l(t,e.data_src)),t.load()}},yt=["IMG","IFRAME","VIDEO"],At={IMG:P,IFRAME:function(t){k(t,ht)},VIDEO:function(t){a(t,function(t){k(t,ht)}),k(t,mt),t.load()}},kt=["IMG","IFRAME","VIDEO"];return t.prototype={update:function(t){var e,n,a,i=this._settings,o=X(t,i);{if(h(this,o.length),!tt&&et)return H(i)?(e=i,n=this,o.forEach(function(t){-1!==kt.indexOf(t.tagName)&&F(t,e,n)}),void h(n,0)):(t=this._observer,i=o,t.disconnect(),a=t,void i.forEach(function(t){a.observe(t)}));this.loadAll(o)}},destroy:function(){this._observer&&this._observer.disconnect(),Q(this._settings).forEach(function(t){y(t)}),delete this._observer,delete this._settings,delete this.loadingCount,delete this.toLoadCount},loadAll:function(t){var e=this,n=this._settings;X(t,n).forEach(function(t){b(t,e),V(t,n,e)})},restoreAll:function(){var e=this._settings;Q(e).forEach(function(t){U(t,e)})}},t.load=function(t,e){e=i(e);V(t,e)},t.resetStatus=function(t){r(t)},Z&&function(t,e){if(e)if(e.length)for(var n,a=0;n=e[a];a+=1)o(t,n);else o(t,e)}(t,window.lazyLoadOptions),t});!function(e,t){"use strict";function a(){t.body.classList.add("litespeed_lazyloaded")}function n(){console.log("[LiteSpeed] Start Lazy Load Images"),d=new LazyLoad({elements_selector:"[data-lazyloaded]",callback_finish:a}),o=function(){d.update()},e.MutationObserver&&new MutationObserver(o).observe(t.documentElement,{childList:!0,subtree:!0,attributes:!0})}var d,o;e.addEventListener?e.addEventListener("load",n,!1):e.attachEvent("onload",n)}(window,document);</script><script data-no-optimize="1">var litespeed_vary=document.cookie.replace(/(?:(?:^|.*;\s*)_lscache_vary\s*\=\s*([^;]*).*$)|^.*$/,"");litespeed_vary||fetch("/wp-content/plugins/litespeed-cache/guest.vary.php",{method:"POST",cache:"no-cache",redirect:"follow"}).then(e=>e.json()).then(e=>{console.log(e),e.hasOwnProperty("reload")&&"yes"==e.reload&&(sessionStorage.setItem("litespeed_docref",document.referrer),window.location.reload(!0))});</script><script data-optimized="1" type="litespeed/javascript" data-src="https://foolishdeveloper.com/wp-content/litespeed/js/6f81178bf1a4fe4ee5072d3287a82103.js?ver=755de"></script><script>const litespeed_ui_events=["mouseover","click","keydown","wheel","touchmove","touchstart"];var urlCreator=window.URL||window.webkitURL;function litespeed_load_delayed_js_force(){console.log("[LiteSpeed] Start Load JS Delayed"),litespeed_ui_events.forEach(e=>{window.removeEventListener(e,litespeed_load_delayed_js_force,{passive:!0})}),document.querySelectorAll("iframe[data-litespeed-src]").forEach(e=>{e.setAttribute("src",e.getAttribute("data-litespeed-src"))}),"loading"==document.readyState?window.addEventListener("DOMContentLoaded",litespeed_load_delayed_js):litespeed_load_delayed_js()}litespeed_ui_events.forEach(e=>{window.addEventListener(e,litespeed_load_delayed_js_force,{passive:!0})});async function litespeed_load_delayed_js(){let t=[];for(var d in document.querySelectorAll('script[type="litespeed/javascript"]').forEach(e=>{t.push(e)}),t)await new Promise(e=>litespeed_load_one(t[d],e));document.dispatchEvent(new Event("DOMContentLiteSpeedLoaded")),window.dispatchEvent(new Event("DOMContentLiteSpeedLoaded"))}function litespeed_load_one(t,e){console.log("[LiteSpeed] Load ",t);var d=document.createElement("script");d.addEventListener("load",e),d.addEventListener("error",e),t.getAttributeNames().forEach(e=>{"type"!=e&&d.setAttribute("data-src"==e?"src":e,t.getAttribute(e))});let a=!(d.type="text/javascript");!d.src&&t.textContent&&(d.src=litespeed_inline2src(t.textContent),a=!0),t.after(d),t.remove(),a&&e()}function litespeed_inline2src(t){try{var d=urlCreator.createObjectURL(new Blob([t.replace(/^(?:<!--)?(.*?)(?:-->)?$/gm,"$1")],{type:"text/javascript"}))}catch(e){d="data:text/javascript;base64,"+btoa(t.replace(/^(?:<!--)?(.*?)(?:-->)?$/gm,"$1"))}return d}</script></body>

</html>
				
			

CSS (style.css)

				
					.sentences-display {
  width: 50%;
  height: 100%;
  margin: 0 0 1em auto;
  padding: 5px;
  overflow: hidden;
  color: grey;
  font-size: 2rem;
  white-space: nowrap;
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 5px;
}

.sentences-display::first-letter {
  color: black;
}

.key {
  display: inline-block;
  min-width: 5%;
  height: 1.5em;
  margin: 0.1em;
  padding-top: 0.1em;
  color: #000;
  font-weight: bold;
  font-weight: 400;
  font-size: 1.5em;
  text-align: center;
  background-color: #fff;
  border: 1px solid #000;
  border-radius: 3px;
  cursor: pointer;
}

.key.active {
  color: #fff;
  background-color: green;
}

section {
  width: 50%;
  margin: 1rem auto;
  color: #000;
  font-weight: bold;
  font-size: 2rem;
  text-align: center;
}

				
			

JavaScript (index.js)

				
					import { VirtualKeyboard } from './VirtualKeyboard.js';
import { sentences } from './sentences.js';
import { Timer } from './Timer.js';

const sentenceDisplayEl = document.querySelector('.sentences-display');
const keyboardEl = document.querySelector('.keyboard');
const timeDisplayEl = document.querySelector('.time-display');
const resetEl = document.querySelector('.reset');

const duration = 60;
let nextLetter;
let totalLetters = 0;
let isOver = false;
const timer = new Timer();

const addSentenceToDisplay = () => {
  const sentence = sentences[Math.floor(Math.random() * sentences.length)];
  sentenceDisplayEl.textContent += sentence + ' ';
};

const udpateSentenceToDisplay = () => {
  if (sentenceDisplayEl.textContent.length < 50) {
    addSentenceToDisplay();
  }

  const nextLetterIndex = sentenceDisplayEl.textContent.charAt(1).trim() ? 1 : 2;
  sentenceDisplayEl.textContent = sentenceDisplayEl.textContent.slice(nextLetterIndex);
  setNextLetter();
};

const setNextLetter = () => {
  nextLetter = sentenceDisplayEl.textContent.charAt(0);
};

const handleTyping = letter => {
  if (isOver) return;

  if (!timer.isTimerRunning && totalLetters === 0) {
    timer.startTimer(duration, displayTime, onComplete);
  }

  if (letter === nextLetter) {
    totalLetters++;
    udpateSentenceToDisplay();
  }
};

const displayTime = time => {
  timeDisplayEl.textContent = time;
};

const onComplete = () => {
  timeDisplayEl.textContent = `You typed ${totalLetters} letters in ${duration} seconds`;
  isOver = true;
};

const onReset = () => {
  sentenceDisplayEl.textContent = '';
  addSentenceToDisplay();
  setNextLetter();
  totalLetters = 0;
  timeDisplayEl.textContent = duration;
  timer.stopTimer();
  isOver = false;
  resetEl.blur();
};

resetEl.addEventListener('click', onReset);

new VirtualKeyboard(keyboardEl, handleTyping);
onReset();

				
			

Sentence.js

				
					export const sentences = [
  'he turned in the research paper on friday otherwise he would have not passed the class',
  'plans for this weekend include turning wine into water',
  'the miniature pet elephant became the envy of the neighborhood',
  'as she walked along the street and looked in the gutter she realized facemasks had become the new cigarette butts',
  'he had unknowingly taken up sleepwalking as a nighttime hobby',
  'they got there early and they got really good seats',
  'david proudly graduated from high school top of his class at age 97',
  'he realized there had been several deaths on this road but his concern rose when he saw the exact number',
  'three generations with six decades of life experience',
  'her life in the confines of the house became her new normal',
  'he never understood why what when and where left out who',
  'truth in advertising and dinosaurs with skateboards have much in common',
  'its not possible to convince a monkey to give you a banana by promising it infinite bananas when they die',
  'he was willing to find the depths of the rabbit hole in order to be with her',
  'the estate agent quickly marked out his territory on the dance floor',
  'at that moment she realized she had a sixth sense',
  'the lyrics of the song sounded like fingernails on a chalkboard',
  'its not often you find a soggy banana on the street',
  'i have traveled all around africa and still havent found the gnu who stole my scarf',
  'although it wasnt a pot of gold nancy was still enthralled at what she found at the end of the rainbow',
  'honestly i didnt care much for the first season so i didnt bother with the second',
  'the balloons floated away along with all my hopes and dreams',
  'the anaconda was the greatest criminal mastermind in this part of the neighborhood',
  'he learned the hardest lesson of his life and had the scars both physical and mental to prove it',
  'he set out for a short walk but now all he could see were mangroves and water were for miles',
  'the spa attendant applied the deep cleaning mask to the gentlemans back',
  'the busker hoped that the people passing by would throw money but they threw tomatoes instead so he exchanged his hat for a juicer',
  'the irony of the situation wasnt lost on anyone in the room',
  'the bug was having an excellent day until he hit the windshield',
  'it would have been a better night if the guys next to us werent in the splash zone',
  'kevin embraced his ability to be at the wrong place at the wrong time',
  'she was amazed by the large chunks of ice washing up on the beach',
  'watching the geriatric mens softball team brought back memories of 3 yr olds playing tball',
  'he used to get confused between soldiers and shoulders but as a military man he now soldiers responsibility',
  'in hopes of finding out the truth he entered the oneroom library',
  'he found his art never progressed when he literally used his sweat and tears',
  'homesickness became contagious in the young campers cabin',
  'today i dressed my unicorn in preparation for the race',
];

				
			

Timer.js

				
					export class Timer {
  isTimerRunning = false;

  startTimer(time, timerCallback, completionCallback) {
    if (this.isTimerRunning) {
      return;
    }

    this.time = time;
    this.timerCallback = timerCallback;
    this.completionCallback = completionCallback;

    this.intervalId = setInterval(this.runTimer, 1000);
    this.isTimerRunning = true;
  }

  stopTimer() {
    clearInterval(this.intervalId);
    this.isTimerRunning = false;
  }

  runTimer = () => {
    if (this.time === 0) {
      this.completionCallback();
      this.stopTimer();
      return;
    }

    this.time--;
    this.timerCallback(this.time);
  };
}

				
			

VirtualKeyboard.js

				
					export class VirtualKeyboard {
  #rowZeroLetters = '1234567890'.split('');
  #rowOneLetters = 'qwertyuiop'.split('');
  #rowTwoLetters = 'asdfghjkl'.split('');
  #rowThreeLetters = 'zxcvbnm'.split('');
  #keyElementMap = new Map();

  constructor(el, callback) {
    this.el = el;
    this.callback = callback;
    this.populateKeys();
    this.addKeyListener();
    this.addKeyboardListener();
  }

  populateKeys() {
    const rows = [this.#rowZeroLetters, this.#rowOneLetters, this.#rowTwoLetters, this.#rowThreeLetters];
    rows.forEach(row => {
      const rowEl = document.createElement('div');
      rowEl.classList.add('row');
      row.forEach(letter => {
        const keyEl = document.createElement('button');
        keyEl.classList.add('key');
        keyEl.textContent = letter;
        rowEl.appendChild(keyEl);
        this.#keyElementMap.set(letter, keyEl);
      });
      this.el.appendChild(rowEl);
    });
  }

  addKeyListener() {
    this.el.addEventListener('click', e => {
      if (e.target.classList.contains('key')) {
        const key = e.target;
        key.classList.add('active');
        setTimeout(() => key.classList.remove('active'), 100);

        this.callback(key.textContent);
      }
    });
  }

  addKeyboardListener() {
    document.addEventListener('keydown', e => {
      let key = e.key;
      this.#keyElementMap.get(key.toLowerCase())?.click();
    });
  }
}

				
			

OUTPUT :

output