Ad – 728Γ—90
πŸ› οΈ Projects

JavaScript Quiz App – Build an Interactive Multiple-Choice Quiz

Build a polished multiple-choice quiz app entirely in vanilla JavaScript. Covers question display, option selection, correct/wrong feedback, score tracking, a results screen with percentage, and full restart functionality β€” all managed through a clean class-based architecture.

⏱️ 28 min read 🎯 Advanced πŸ“… Updated 2026

Question Data Structure

The quiz data is an array of objects. Each question has a text, an array of options, and the index of the correct answer. This structure is easy to extend, load from an API, or store in JSON.

JavaScript
// quiz-data.js (or inline in quiz-script.js)

const questions = [
  {
    question: "Which keyword declares a block-scoped variable in JavaScript?",
    options: ["var", "let", "def", "local"],
    correct: 1
  },
  {
    question: "What does the Array method .map() return?",
    options: ["The original array (mutated)", "A new array", "undefined", "An object"],
    correct: 1
  },
  {
    question: "Which of these is NOT a JavaScript primitive?",
    options: ["string", "boolean", "array", "null"],
    correct: 2
  },
  {
    question: "What does 'use strict' do?",
    options: [
      "Prevents variable hoisting",
      "Enables strict type checking",
      "Enables a stricter parsing and error handling mode",
      "Disables closures"
    ],
    correct: 2
  },
  {
    question: "What is the output of: typeof null?",
    options: ["null", "undefined", "object", "string"],
    correct: 2
  },
  {
    question: "Which method removes the last element from an array?",
    options: [".shift()", ".splice(-1)", ".pop()", ".slice(-1)"],
    correct: 2
  },
  {
    question: "What is a closure?",
    options: [
      "A way to close the browser tab",
      "A function that retains access to its outer scope after the outer function returns",
      "A method for ending async operations",
      "A type of loop"
    ],
    correct: 1
  },
  {
    question: "Which symbol is used for template literals?",
    options: ["Single quote (')", "Double quote (\")", "Backtick (`)", "Tilde (~)"],
    correct: 2
  },
  {
    question: "What does Promise.all() do when one promise rejects?",
    options: [
      "Ignores the rejection and continues",
      "Resolves with null for the failed promise",
      "Immediately rejects with the first rejection reason",
      "Waits for all promises then reports failures"
    ],
    correct: 2
  },
  {
    question: "How do you create a deep clone of an object in modern JavaScript?",
    options: [
      "Object.assign({}, obj)",
      "{ ...obj }",
      "structuredClone(obj)",
      "JSON.copy(obj)"
    ],
    correct: 2
  }
];

HTML Structure

JavaScript
/* quiz.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JavaScript Quiz</title>
  <link rel="stylesheet" href="quiz-style.css">
</head>
<body>
  <div class="quiz-container">

    <!-- Quiz Screen -->
    <div id="quiz-screen">
      <div class="quiz-header">
        <span id="question-counter">Question 1 / 10</span>
        <span id="score-display">Score: 0</span>
      </div>
      <div class="progress-bar"><div id="progress-fill"></div></div>
      <p id="question-text" class="question-text"></p>
      <div id="options-grid" class="options-grid"></div>
      <button id="next-btn" class="next-btn hidden">Next Question β†’</button>
    </div>

    <!-- Results Screen -->
    <div id="results-screen" class="hidden">
      <h2>Quiz Complete!</h2>
      <div class="score-circle">
        <span id="final-score"></span>
        <small>out of 10</small>
      </div>
      <p id="score-message"></p>
      <div id="results-breakdown"></div>
      <button id="restart-btn">Try Again</button>
    </div>

  </div>
  <script src="quiz-script.js"></script>
</body>
</html>
*/

Quiz Class Architecture

Encapsulating all quiz state and methods inside a Quiz class prevents global namespace pollution and keeps related logic together β€” a clean OOP approach.

JavaScript
// quiz-script.js

class Quiz {
  #questions;
  #currentIndex;
  #score;
  #answered;
  #userAnswers;

  constructor(questions) {
    this.#questions  = this.#shuffle([...questions]); // shuffle a copy
    this.#currentIndex = 0;
    this.#score      = 0;
    this.#answered   = false;
    this.#userAnswers = [];
  }

  get currentQuestion() {
    return this.#questions[this.#currentIndex];
  }

  get progress() {
    return ((this.#currentIndex) / this.#questions.length) * 100;
  }

  get isFinished() {
    return this.#currentIndex >= this.#questions.length;
  }

  get score()     { return this.#score; }
  get total()     { return this.#questions.length; }
  get answered()  { return this.#answered; }
  get userAnswers() { return this.#userAnswers; }

  answer(optionIndex) {
    if (this.#answered) return null; // prevent double answers
    this.#answered = true;
    const isCorrect = optionIndex === this.currentQuestion.correct;
    if (isCorrect) this.#score++;
    this.#userAnswers.push({
      question: this.currentQuestion.question,
      chosen:   optionIndex,
      correct:  this.currentQuestion.correct,
      isCorrect
    });
    return isCorrect;
  }

  next() {
    this.#currentIndex++;
    this.#answered = false;
  }

  #shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
  }
}

UI Controller

JavaScript
// UI Controller β€” all DOM interactions

let quiz; // will be initialised in startQuiz()

// DOM refs
const quizScreen    = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const questionText  = document.getElementById("question-text");
const optionsGrid   = document.getElementById("options-grid");
const nextBtn       = document.getElementById("next-btn");
const counterEl     = document.getElementById("question-counter");
const scoreEl       = document.getElementById("score-display");
const progressFill  = document.getElementById("progress-fill");

function renderQuestion() {
  const q = quiz.currentQuestion;

  counterEl.textContent  = `Question ${quiz.userAnswers.length + 1} / ${quiz.total}`;
  scoreEl.textContent    = `Score: ${quiz.score}`;
  progressFill.style.width = `${quiz.progress}%`;
  questionText.textContent = q.question;
  nextBtn.classList.add("hidden");

  optionsGrid.innerHTML = q.options.map((option, i) => `
    <button class="option-btn" data-index="${i}">
      <span class="option-letter">${String.fromCharCode(65 + i)}</span>
      ${option}
    </button>
  `).join("");
}

function handleAnswer(optionIndex) {
  const isCorrect = quiz.answer(optionIndex);
  if (isCorrect === null) return; // already answered

  const buttons = optionsGrid.querySelectorAll(".option-btn");

  // Show feedback on all options
  buttons.forEach((btn, i) => {
    btn.disabled = true;
    if (i === quiz.currentQuestion.correct) {
      btn.classList.add("correct");
    } else if (i === optionIndex && !isCorrect) {
      btn.classList.add("incorrect");
    }
  });

  scoreEl.textContent = `Score: ${quiz.score}`;
  nextBtn.classList.remove("hidden");
  nextBtn.textContent = quiz.userAnswers.length >= quiz.total
    ? "See Results" : "Next Question β†’";
}

function showResults() {
  quizScreen.classList.add("hidden");
  resultsScreen.classList.remove("hidden");

  const { score, total, userAnswers } = quiz;
  const percentage = Math.round((score / total) * 100);

  document.getElementById("final-score").textContent = score;
  document.getElementById("score-message").textContent =
    percentage >= 80 ? "Excellent! You really know JavaScript!" :
    percentage >= 60 ? "Good job! Keep practising." :
    "Keep learning! Review the topics and try again.";

  // Results breakdown
  document.getElementById("results-breakdown").innerHTML = userAnswers.map(a => `
    <div class="breakdown-item ${a.isCorrect ? "correct" : "incorrect"}">
      <span class="breakdown-icon">${a.isCorrect ? "βœ…" : "❌"}</span>
      <div>
        <p class="breakdown-question">${a.question}</p>
        ${!a.isCorrect
          ? `<p class="breakdown-answer">Correct: ${quiz.currentQuestion?.options[a.correct] ?? "β€”"}</p>`
          : ""}
      </div>
    </div>
  `).join("");
}

// Event listeners
optionsGrid.addEventListener("click", e => {
  const btn = e.target.closest(".option-btn");
  if (btn) handleAnswer(Number(btn.dataset.index));
});

nextBtn.addEventListener("click", () => {
  quiz.next();
  if (quiz.isFinished) {
    showResults();
  } else {
    renderQuestion();
  }
});

document.getElementById("restart-btn").addEventListener("click", startQuiz);

// Keyboard navigation (1–4 for options, Enter/Space for next)
document.addEventListener("keydown", e => {
  if (quizScreen.classList.contains("hidden")) return;
  const num = parseInt(e.key);
  if (num >= 1 && num <= 4) handleAnswer(num - 1);
  if ((e.key === "Enter" || e.key === " ") && !nextBtn.classList.contains("hidden")) {
    nextBtn.click();
  }
});

// Initialize
function startQuiz() {
  quiz = new Quiz(questions);
  quizScreen.classList.remove("hidden");
  resultsScreen.classList.add("hidden");
  renderQuestion();
}

startQuiz();
Behavior: 10 questions display one at a time. Clicking an option highlights it green (correct) or red (incorrect) and reveals the correct answer. "Next Question" advances the quiz. The results screen shows score out of 10, a percentage message, and a per-question breakdown.
Private Fields: The Quiz class uses private fields (#questions, #score, etc.) introduced in ES2022. This ensures the game state can only be modified through the public answer() and next() methods β€” preventing accidental state corruption from outside the class.
Concepts Practiced: ES6 classes, private class fields, array shuffling (Fisher-Yates), closures for state, event delegation, keyboard accessibility, conditional rendering, and data-driven UI patterns.
Ad – 336Γ—280

CSS Highlights

JavaScript
/* quiz-style.css β€” key styles

.option-btn {
  padding: 16px;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.2s;
  text-align: left;
  font-size: 15px;
}
.option-btn:hover:not(:disabled) {
  border-color: #3b82f6;
  background: #eff6ff;
}
.option-btn.correct   { background: #d1fae5; border-color: #10b981; color: #065f46; }
.option-btn.incorrect { background: #fee2e2; border-color: #ef4444; color: #991b1b; }
.option-btn:disabled  { cursor: not-allowed; }
.progress-bar { height: 6px; background: #e2e8f0; border-radius: 3px; margin: 12px 0; }
#progress-fill { height: 100%; background: #3b82f6; border-radius: 3px; transition: width 0.4s; }
.score-circle {
  width: 120px; height: 120px;
  border-radius: 50%;
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  color: white; font-size: 40px; font-weight: 700;
  margin: 24px auto;
}
*/

Enhancements

Extend the Quiz App

  • Timer β€” add a countdown timer per question (e.g., 30 seconds); auto-advance on timeout
  • Categories β€” add a category field to questions and let the user choose a topic
  • High score β€” store the best score in localStorage and display it on the results screen
  • Lifelines β€” 50/50 lifeline that removes two wrong options
  • Fetch questions β€” load questions from the free Open Trivia DB API (opentdb.com)

Timer Implementation

Add a 30-second countdown to the Quiz class and UI:

// In the Quiz class:
#timeLimit = 30;
#timeLeft  = 30;
#timerId   = null;

startTimer(onTick, onExpire) {
  this.#timeLeft = this.#timeLimit;
  this.#timerId  = setInterval(() => {
    this.#timeLeft--;
    onTick(this.#timeLeft);
    if (this.#timeLeft <= 0) {
      clearInterval(this.#timerId);
      onExpire();
    }
  }, 1000);
}

stopTimer() { clearInterval(this.#timerId); }

Concepts Breakdown

ConceptWhere Applied
ES6 ClassQuiz class encapsulates all game logic
Private Fields (#)Prevent external state mutation
Fisher-Yates Shuffle#shuffle() randomizes question order
ClosuresEvent handlers capture quiz variable
Event DelegationOptions grid click handler
Keyboard EventsPress 1–4 to select options
Conditional Renderingshow/hide quiz vs results screen
Data-driven UIHTML generated from question objects

FAQ

Why shuffle the questions using a copy of the array?

Shuffling the original questions array in-place would mutate the source data. Creating a copy with [...questions] means the original is preserved and restarting the quiz produces a new shuffle each time.

Why use a class instead of plain functions and variables?

Classes group related state and behavior together, making the code easier to understand, test, and extend. Private fields prevent bugs from accidental external modification. For a larger quiz app (multiple quiz types, difficulty levels), the class architecture scales cleanly.

How do I load questions from an external API?

Open Trivia DB (opentdb.com) provides free quiz questions as JSON: https://opentdb.com/api.php?amount=10&type=multiple. Fetch on app load and map the response to your question format.

How does keyboard navigation improve accessibility?

Users who prefer keyboards (power users, accessibility needs) can answer with number keys 1–4 and advance with Enter or Space without touching the mouse. Ensure answer buttons also have proper aria attributes for screen reader support.

How do I add multiple quiz topics?

Add a category field to each question object. Show a topic selection screen before the quiz starts. Filter questions by selected category: questions.filter(q => q.category === selected).

Summary

  • Data structure: array of question objects with text, options array, and correct index
  • Quiz class: encapsulates state using private fields, exposes only controlled mutations
  • Fisher-Yates shuffle: randomizes question order on every new game
  • UI controller: reads from the Quiz instance and renders DOM based on current state
  • Results screen: per-question breakdown with correct answers shown for wrong guesses
  • Class-based quiz architecture demonstrates OOP knowledge applied to a real feature
  • Fisher-Yates shuffle is a classic algorithm question β€” knowing it by name impresses interviewers
  • Private fields show awareness of modern JavaScript features (ES2022)
  • Separating data (questions array), logic (Quiz class), and UI (controller) demonstrates separation of concerns