DEV Community

Codewithrandom Blogs
Codewithrandom Blogs

Posted on

Quiz App with Timer using HTML CSS & JavaScript

Hello readers, Today in this blog you’ll learn how to Create a Quiz App with Timer using HTML CSS & JavaScript.

My goal is to take you on a trip from planning to building a Quiz App. For that, we will use vanilla JavaScript, CSS, and HTML. No additional libraries or packages. Let’s get started by defining what our Quiz App can do.

The Quiz App will be split into two main classes. The first one will be a settings area in which the player can choose the difficulty, the category, and the number of questions he wants to answer. For that, we will create a settings-class to track all of this information. After doing that he can start the quiz.

The second area will be a quiz. The quiz-class tracks the progress of the player and decides whether or not to display the next question of the final screen.

Furthermore, the quiz-class has two other components, first of an array of question-classes that hold the data of a question, display it, and checks if the answer was right or not. The other one is the final-class that displays the last page with the player’s score.

We will be using the Open Trivia DB API for the questions so that we don’t have to come up with our questions.

HTML Code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla Quiz</title>
    <link rel="stylesheet" href="styles.css" />
    <link
      href="https://fonts.googleapis.com/css2?family=Questrial&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <main>
      <div class="header">
        <h2>Vanilla Quiz</h2>
      </div>
      <div class="main">
        <div class="final">
          <h3>You answered all of the questions!</h3>
          <p>Score:</p>
          <p class="score"></p>
          <h4>Want to try it again?</h4>
          <button id="again" class="submit">Again</button>
        </div>
        <div class="quiz">
          <div class="count">
            <p class="current">0</p>
            <p style="margin-left: 40px">/</p>
            <p class="total"></p>
          </div>
          <h3 id="question"></h3>
          <label id="a1" class="container">
            <input type="radio" checked="checked" name="radio" />
            <span class="checkmark"></span>
          </label>
          <label id="a2" class="container">
            <input type="radio" name="radio" />
            <span class="checkmark"></span>
          </label>
          <label id="a3" class="container">
            <input type="radio" name="radio" />
            <span class="checkmark"></span>
          </label>
          <label id="a4" class="container">
            <input type="radio" name="radio" />
            <span class="checkmark"></span>
          </label>
          <button id="next" class="submit">Submit</button>
        </div>
        <div class="settings">
          <h3 style="text-align: center">Set up your Quiz!</h3>
          <label for="category">Category</label>
          <select name="category" id="category">
            <option value="9">General Knowledge</option>
            <option value="27">Animals</option>
            <option value="15">Video Games</option>
            <option value="23">History</option>
            <option value="21">Sports</option>
          </select>
          <div class="mt30">
            <label for="difficulty">Difficulty</label>
            <label class="container" style="display: inline; margin-left: 30px"
              >Easy
              <input type="radio" name="radio" id="easy" />
              <span class="checkmark" style="margin-top: 2px"></span>
            </label>
            <label class="container" style="display: inline; margin-left: 30px"
              >Medium
              <input type="radio" name="radio" id="medium" />
              <span class="checkmark" style="margin-top: 2px"></span>
            </label>
            <label class="container" style="display: inline; margin-left: 30px"
              >Hard
              <input type="radio" name="radio" id="hard" />
              <span class="checkmark" style="margin-top: 2px"></span>
            </label>
          </div>
          <div class="mt30">
            <label for="questions">Number of questions</label>
            <input
              name="questions"
              id="questions"
              type="text"
              pattern="[0-9]*"
            />
          </div>
          <button id="start" class="submit">Start</button>
        </div>
      </div>
    </main>
    <script type="module" src="index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

The code starts with the tag, which contains meta tags that define the page's title and author. The next line is a link to styles.css, which defines how the website looks. The code then starts with an HTML5 doctype declaration: This tells browsers that this document is written in HTML5 and not older versions of HTML like XHTML or even XML-based markup languages like SVG or MathML.

It also declares what character encoding should be used for rendering this document (UTF-8). This is important because different countries use different encodings when they view websites on their computers; UTF-8 allows all users to view webpages without having to change anything about their computer settings.

Next comes a meta name="viewport" tag, which sets up how wide the browser window will be displayed at its initial scale (1) before it scales itself based on device widths once it loads more content into its buffer zone (device-width). This prevents mobile devices from displaying too small of text while desktop computers display too large of text by default since they have larger screens than mobile devices do.

CSS Code

:root {
  --primary-color: #5D737E;
  --secondary-color: #D6F8D6;
  --tertiary-color: #7FC6A4;
  --quaternary-color: #55505C;
  --hover-color: #4e616b;
  --shadow-color:rgba(57, 127, 93, 0.4);
  --font-style: 'Questrial';
}

body {
  font-family: var(--font-style), 'Ranchers', cursive;
  background-color: var(--secondary-color);
  width: 100vw;
  height: 100vh;
  justify-content: center;
  align-items: center;
}

h2 {
  font-size: 3.5rem;
  text-align: center;
  color: var(--primary-color);
}

.mt30 {
  margin-top: 30px;
}

.header {
  padding: 15px;
}

.main {
  display: flex;
  justify-content: center;
}

.settings {

  z-index: 1;
}

.final {
  visibility: hidden;
  z-index: 2;
}

.final p {
  font-size: 30px;
  text-align: center;
}

.final h4 {
  font-size: 33px;
  text-align: center;
}

.quiz  {
  visibility: hidden;
  z-index: 0;
}

#questions {
  font-size: 20px;
  font-family: var(--font-style), 'Ranchers', cursive;
  font-weight: 600;
  line-height: 1.3;
  color: white;
  background-color: var(--primary-color);
  appearance: none;
  border: none;
  padding: 5px;
  border-radius: 5px;
  margin-left: 30px;
  outline: none;
  text-align: center;
  width: 120px;
}
.settings select {
  font-size: 20px;
  font-family: var(--font-style), 'Ranchers', cursive;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 1px;
  color: white;
  background-color: var(--primary-color);
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance: none;
  border: none;
  padding: 5px;
  border-radius: 5px;
  margin-left: 20px;
  outline: none;
  text-align: center;
}

.settings select::-ms-expand {
  display: none;
}

.settings select:hover {
  border-color: var(--hover-color);
}

.settings select:focus {
  border-color: var(--hover-color);
}

.settings select option {
  /* font-weight: bolder; */
  font-family: var(--font-style), 'Ranchers', sans-serif;
}

.settings label {
  font-size: 25px;
  margin-right: 16px;
}

.quiz, .settings, .final {
  position: absolute;
  padding: 0px 35px 35px 35px;
  max-width: 560px;
  background-color: var(--tertiary-color);
  border-radius: 7px;
  -webkit-box-shadow: 10px 10px 3px -4px var(--shadow-color);
  -moz-box-shadow: 10px 10px 3px -4px var(--shadow-color);
  box-shadow: 10px 10px 5px -4px var(--shadow-color);
}

h3 {
  display: block;
  width: 550px;
  font-size: 35px;
  font-weight: 350;
  word-wrap: break-word;
}

.submit {
  width: 100%;
  color: white;
  background-color: var(--primary-color);
  font-family: var(--font-style), 'Ranchers', cursive;
  outline: none;
  border: none;
  height: 50px;
  font-size: 1.8rem;
  margin-top: 20px;
  border-radius: 5px;
  letter-spacing: 2px;
}

.submit:hover {
  background-color: var(--hover-color);
  cursor: pointer;
  color: #FAF33E;
}

/* The container */
.count {
  display: block;
  left: 75%;
  position: relative;
  padding-left: 35px;
  margin-bottom: 100px;
  cursor: pointer;
}

.count p {
  position: absolute;
  font-size: 35px;

}

.total {
  margin-left: 50px;
}

/* The container */
.container {
  display: block;
  position: relative;
  padding-left: 35px;
  margin-bottom: 12px;
  cursor: pointer;
  font-size: 25px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

/* Hide the browser's default radio button */
.container input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

/* Create a custom radio button */
.checkmark {
  position: absolute;
  top: -2px;
  left: 0px;
  height: 25px;
  width: 25px;
  background-color: white;
  border-radius: 30%;
}

/* On mouse-over, add a grey background color */
.container:hover input ~ .checkmark {
  background-color: #FAF33E;
}

/* When the radio button is checked, add a blue background */
.container input:checked ~ .checkmark {
  background-color: var(--quaternary-color);
}

/* Create the indicator (the dot/circle - hidden when not checked) */
.checkmark:after {
  content: "";
  position: absolute;
  display: none;
}

/* Show the indicator (dot/circle) when checked */
.container input:checked ~ .checkmark:after {
  display: block;
}

## JavaScript Code

question.js
class Question {
  constructor(question) {
    this.questionElement = document.querySelector('#question');
    this.answerElements = [
      document.querySelector('#a1'),
      document.querySelector('#a2'),
      document.querySelector('#a3'),
      document.querySelector('#a4'),
    ];

    this.correctAnswer = question.correct_answer;
    this.question = question.question;
    this.isCorrect = false;

    this.answers = this.shuffleAnswers([
      question.correct_answer, 
      ...question.incorrect_answers
    ]);
  }

  shuffleAnswers(answers) {
    for (let i = answers.length - 1; i > 0; i--){
      const j = Math.floor(Math.random() * i)
      const temp = answers[i]
      answers[i] = answers[j]
      answers[j] = temp
    }
    return answers;
  }

  answer(checkedElement) {
     this.isCorrect = (checkedElement[0].textContent === this.correctAnswer) ? true : false;
  }

  render() {
    this.questionElement.innerHTML = this.question;
    this.answerElements.forEach((el, index) => {
      el.innerHTML = '<input type="radio" name="radio"><span class="checkmark"></span>' + this.answers[index];
    });
  }
}

export default Question;
final.js
class Final {
  constructor(count, totalAmount) {
    this.scoreElement = document.querySelector('.score');
    this.againButton = document.querySelector('#again');


    this.render(count, totalAmount);
    this.againButton.addEventListener('click', location.reload.bind(location));
  }

  render(count, totalAmount) {
    this.scoreElement.innerHTML = `You answered ${count} out of ${totalAmount} correct!`;
  }
}

export default Final;

quiz.js

import Final from './final.js';
import Question from './question.js'

class Quiz {
  constructor(quizElement, amount, questions) {
    this.quizElement = quizElement;
    this.currentElement = document.querySelector('.current');
    this.totalElement = document.querySelector('.total');
    this.nextButton = document.querySelector('#next');
    this.finalElement = document.querySelector('.final')

    this.totalAmount = amount;
    this.answeredAmount = 0;
    this.questions = this.setQuestions(questions);

    this.nextButton.addEventListener('click', this.nextQuestion.bind(this));
    this.renderQuestion();
  }

  setQuestions(questions) {
    return questions.map(question => new Question(question));
  }

  renderQuestion() {
    this.questions[this.answeredAmount].render();
    this.currentElement.innerHTML = this.answeredAmount;
    this.totalElement.innerHTML = this.totalAmount;
  }

  nextQuestion() {
    const checkedElement = this.questions[this.answeredAmount].answerElements.filter(el => el.firstChild.checked);
    if (checkedElement.length === 0) {
      alert('You need to select an answer');
    } else {
      this.questions[this.answeredAmount].answer(checkedElement)
      this.showResult();
      this.answeredAmount++;
      (this.answeredAmount < this.totalAmount) ? this.renderQuestion() : this.endQuiz();
    }
  }

  showResult() {
    this.questions[this.answeredAmount].isCorrect ? alert('Correct answer :)') : alert('Wrong answer :(');
  }

  endQuiz() {
    this.quizElement.style.visibility = 'hidden';
    this.finalElement.style.visibility = 'visible';
    const correctAnswersTotal = this.calculateCorrectAnswers();
    this.final = new Final(correctAnswersTotal, this.totalAmount);
  }

  calculateCorrectAnswers() {
    let count = 0;
    this.questions.forEach(el => {
      if (el.isCorrect) {
        count++;
      }
    });
    return count;
  }
}

export default Quiz;
settings.js
import Quiz from './quiz.js';

class Settings {
  constructor() {
    this.quizElement = document.querySelector('.quiz');
    this.settingsElement = document.querySelector('.settings');
    this.category = document.querySelector('#category');
    this.numberOfQuestions = document.querySelector('#questions');
    this.difficulty = [
      document.querySelector('#easy'),
      document.querySelector('#medium'),
      document.querySelector('#hard'),
    ];
    this.startButton = document.querySelector('#start');

    this.quiz = { };

    this.startButton.addEventListener('click', this.startQuiz.bind(this));
  }

  async startQuiz() {
    try {
      const amount = this.getAmount();
      const categoryId = this.category.value;
      const difficulty = this.getCurrentDifficulty();

      const url = `https://opentdb.com/api.php?amount=${amount}&category=${categoryId}&difficulty=${difficulty}&type=multiple`;

      let data = await this.fetchData(url);
      this.toggleVisibility();
      this.quiz = new Quiz(this.quizElement, amount, data.results);
    } catch (error) {
      alert(error);
    }
  }

  toggleVisibility() {
    this.settingsElement.style.visibility = 'hidden';
    this.quizElement.style.visibility = 'visible';
  }

  async fetchData(url) {
    const response = await fetch(url);
    const result = await response.json();

    return result;
  }

  getCurrentDifficulty() {
    const checkedDifficulty = this.difficulty.filter(element => element.checked);

    if (checkedDifficulty.length === 1) {
      return checkedDifficulty[0].id;
    } else {
      throw new Error('Please select a difficulty!');
    }
  }

  getAmount() {
    const amount = this.numberOfQuestions.value;
    // Not negative, not 0 and not over 50
    if (amount > 0 && amount < 51) {
      return amount;
    }
    throw new Error('Please enter a number of questions between 1 and 50!');
  }
}

export default Settings;

index.js

import Settings from './quiz/settings.js';

new Settings();
Enter fullscreen mode Exit fullscreen mode

What are we doing here?

First, we get all three values, for the amount and difficulty we use methods that are not yet implemented. In these methods, we will be handling errors e.g. not choosing any difficulty or entering a negative number for the number of questions.

After that, we create the URL with the parameters we just got. This URL is passed in the fetchData-method which will send the request and returns the data. After that, we call toggleVisibility and initialize a new quiz-object by passing in the result, amount, and the quizElement.

If at any point an error is thrown we will catch it and display it by using the alert-method.

Both methods getAmount and getCurrentDifficulty are returning an error if the player did not select anything or the selected value is out of bounds (for the number of questions). We also added the import-statement for the quiz-class at the top of this file. The other two methods (fetchData and toggleVisibility) do exactly what their names suggest. Now we can focus on the quiz-class next.

This time we have some arguments that got passed in by the settings-object that we need to deal with. For the questions, we create a single question-object for each question that got passed in by the settings-object. The constructor needs some more set up so we will add some more DOM-Elements and an event-listener to the nextButton too. So let’s go ahead and do this!

The next step is to implement the shuffleAnswers, answer, and render methods. For the shuffling of the array, we will use the Fisher-Yates-Shuffle-Algorithm.

The answer-method will just compare the choice of the player with the correctAnswer property and the render method will display the question and all the possible answers. For this to work, we need to get the respective DOM-Elements and end up with this question.js:

Now the only thing missing is the final-class. This class is really simple we just need to get the DOM-Elements to display the final result to the player. To add some convenience we can add an again-button that reloads the page so the player can start again.

The quiz app is now complete. We implemented this with just plain old JavaScript and used the concept of Object-Oriented-Programming. I hope you enjoyed this and as always you can find the code on my GitHub.
   
You have completed learning Quiz App with Timer Using Html, CSS, And JavaScript. I hope I have explained to you in this tutorial how I created this Quiz App with Timer with the help of JavaScript.

If you enjoyed reading this post and have found it useful for you, then please give a share with your friends, and follow me to get updates on my upcoming posts. You can connect with me on Instagram.

if you have any confusion Comment below or you can contact us by filling out our contact us form from the home section.

written by – Ninja_webTech

Top comments (0)