DocsHub
JavascriptAdvanced

Quiz Engine

Build a fully functional quiz engine with multiple choice questions, scoring, a timer, and a results screen.

Quiz Engine

Problem

Build a quiz engine that presents multiple choice questions one at a time. The user selects an answer, gets immediate feedback, and moves to the next question. At the end a results screen shows the score and lets the user retry.

Page loads
→ Welcome screen with "Start Quiz" button

User clicks Start
→ First question appears
→ 30 second countdown timer starts

User selects correct answer
→ Answer highlighted green
→ Score increases
→ "Next" button appears

User selects wrong answer
→ Wrong answer highlighted red
→ Correct answer highlighted green
→ "Next" button appears

Timer runs out
→ Question locked
→ Correct answer revealed
→ "Next" button appears

Last question answered
→ Results screen shows:
   Score: 7/10 (70%)
   Time taken
   Performance message
→ "Try Again" button

Logic

  1. Store questions in an array — each with a question, options, and correct index
  2. Track quiz state — current question index, score, selected answer, timer
  3. Render one question at a time
  4. On answer select — lock all options, highlight correct and wrong
  5. Run a 30 second countdown timer per question
  6. When timer hits 0 — auto-lock and reveal the correct answer
  7. Show results when all questions are answered
  8. Save best score to localStorage

Flow

answer selected timer out no yes yes Start Quiz clicked Reset state Render question 1 Start 30s timer User selects answeror timer runs out Lock all options Highlight correct and wrong Show Next button Last question? Increment question index Show results screen User clicks Try Again?

HTML Structure

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Quiz Engine</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        font-family: sans-serif;
        min-height: 100vh;
        background: #f0f2f5;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
      }

      /* main quiz card */
      .card {
        background: #fff;
        border-radius: 16px;
        padding: 36px 32px;
        width: 100%;
        max-width: 520px;
        box-shadow: 0 2px 16px rgba(0,0,0,0.08);
      }

      /* welcome screen */
      #welcome-screen {
        text-align: center;
      }

      #welcome-screen h1 {
        font-size: 1.8rem;
        margin-bottom: 12px;
        color: #111;
      }

      #welcome-screen p {
        color: #888;
        margin-bottom: 8px;
        font-size: 0.95rem;
      }

      .meta-info {
        display: flex;
        justify-content: center;
        gap: 20px;
        margin: 20px 0;
        font-size: 0.85rem;
        color: #888;
      }

      .meta-item {
        display: flex;
        align-items: center;
        gap: 5px;
      }

      /* quiz screen */
      #quiz-screen {
        display: none;
      }

      /* header bar */
      .quiz-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 24px;
      }

      .question-number {
        font-size: 0.85rem;
        color: #888;
        font-weight: 500;
      }

      /* timer */
      .timer {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 0.9rem;
        font-weight: 700;
        color: #111;
        background: #f5f5f5;
        padding: 6px 12px;
        border-radius: 20px;
      }

      .timer.warning {
        color: #e67e22;
        background: #fff3e0;
      }

      .timer.danger {
        color: #e74c3c;
        background: #fff0f0;
      }

      /* progress bar */
      .progress-bar {
        height: 4px;
        background: #f0f0f0;
        border-radius: 2px;
        margin-bottom: 28px;
        overflow: hidden;
      }

      .progress-fill {
        height: 100%;
        background: #111;
        border-radius: 2px;
        transition: width 0.3s ease;
      }

      /* question text */
      .question-text {
        font-size: 1.15rem;
        font-weight: 600;
        color: #111;
        line-height: 1.5;
        margin-bottom: 24px;
      }

      /* answer options */
      .options {
        display: flex;
        flex-direction: column;
        gap: 10px;
        margin-bottom: 24px;
      }

      .option-btn {
        width: 100%;
        padding: 14px 16px;
        text-align: left;
        border: 2px solid #e8e8e8;
        border-radius: 10px;
        background: #fff;
        font-size: 0.95rem;
        cursor: pointer;
        color: #333;
        transition: border-color 0.15s, background 0.15s;
        display: flex;
        align-items: center;
        gap: 12px;
      }

      .option-btn:hover:not(:disabled) {
        border-color: #aaa;
        background: #fafafa;
      }

      /* option letter badge */
      .option-letter {
        width: 28px;
        height: 28px;
        border-radius: 50%;
        background: #f0f0f0;
        color: #555;
        font-size: 0.8rem;
        font-weight: 700;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
        transition: background 0.15s, color 0.15s;
      }

      /* correct answer style */
      .option-btn.correct {
        border-color: #1a7a3f;
        background: #e6f9ee;
      }

      .option-btn.correct .option-letter {
        background: #1a7a3f;
        color: #fff;
      }

      /* wrong answer style */
      .option-btn.wrong {
        border-color: #c0392b;
        background: #fff0f0;
      }

      .option-btn.wrong .option-letter {
        background: #c0392b;
        color: #fff;
      }

      /* disabled after answer */
      .option-btn:disabled {
        cursor: not-allowed;
      }

      /* feedback message */
      #feedback {
        font-size: 0.9rem;
        font-weight: 500;
        margin-bottom: 16px;
        min-height: 22px;
      }

      #feedback.correct {
        color: #1a7a3f;
      }

      #feedback.wrong {
        color: #c0392b;
      }

      #feedback.timeout {
        color: #e67e22;
      }

      /* next button */
      #next-btn {
        width: 100%;
        padding: 14px;
        font-size: 1rem;
        font-weight: 600;
        background: #111;
        color: #fff;
        border: none;
        border-radius: 10px;
        cursor: pointer;
        display: none;
      }

      #next-btn:hover {
        opacity: 0.85;
      }

      /* results screen */
      #results-screen {
        display: none;
        text-align: center;
      }

      .score-circle {
        width: 120px;
        height: 120px;
        border-radius: 50%;
        background: #f5f5f5;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin: 0 auto 24px;
      }

      .score-circle .score-number {
        font-size: 2.2rem;
        font-weight: 800;
        color: #111;
        line-height: 1;
      }

      .score-circle .score-label {
        font-size: 0.75rem;
        color: #888;
        margin-top: 4px;
      }

      #results-screen h2 {
        font-size: 1.4rem;
        margin-bottom: 8px;
        color: #111;
      }

      #performance-msg {
        color: #888;
        font-size: 0.95rem;
        margin-bottom: 24px;
      }

      .result-stats {
        display: flex;
        gap: 12px;
        margin-bottom: 28px;
      }

      .result-stat {
        flex: 1;
        background: #f5f5f5;
        border-radius: 10px;
        padding: 14px;
      }

      .result-stat .rs-value {
        font-size: 1.3rem;
        font-weight: 700;
        color: #111;
      }

      .result-stat .rs-label {
        font-size: 0.75rem;
        color: #999;
        margin-top: 3px;
      }

      #best-score-text {
        font-size: 0.82rem;
        color: #aaa;
        margin-bottom: 20px;
      }

      #retry-btn {
        width: 100%;
        padding: 14px;
        font-size: 1rem;
        font-weight: 600;
        background: #111;
        color: #fff;
        border: none;
        border-radius: 10px;
        cursor: pointer;
      }

      #retry-btn:hover {
        opacity: 0.85;
      }

      /* start button */
      #start-btn {
        margin-top: 24px;
        padding: 14px 40px;
        font-size: 1rem;
        font-weight: 600;
        background: #111;
        color: #fff;
        border: none;
        border-radius: 10px;
        cursor: pointer;
      }

      #start-btn:hover {
        opacity: 0.85;
      }
    </style>
  </head>
  <body>
    <div class="card">

      <!-- Welcome Screen -->
      <div id="welcome-screen">
        <h1>JavaScript Quiz</h1>
        <p>Test your JavaScript knowledge across 10 questions.</p>
        <div class="meta-info">
          <div class="meta-item">📝 10 Questions</div>
          <div class="meta-item">⏱ 30s per question</div>
          <div class="meta-item">🏆 Scored</div>
        </div>
        <button id="start-btn">Start Quiz</button>
      </div>

      <!-- Quiz Screen -->
      <div id="quiz-screen">
        <div class="quiz-header">
          <span class="question-number" id="question-number">Question 1 of 10</span>
          <div class="timer" id="timer">⏱ 30s</div>
        </div>

        <div class="progress-bar">
          <div class="progress-fill" id="progress-fill" style="width:0%"></div>
        </div>

        <div class="question-text" id="question-text"></div>

        <div class="options" id="options-container"></div>

        <div id="feedback"></div>

        <button id="next-btn">Next Question →</button>
      </div>

      <!-- Results Screen -->
      <div id="results-screen">
        <div class="score-circle">
          <div class="score-number" id="score-number">0/10</div>
          <div class="score-label">Score</div>
        </div>
        <h2 id="results-title">Quiz Complete!</h2>
        <p id="performance-msg"></p>
        <div class="result-stats">
          <div class="result-stat">
            <div class="rs-value" id="stat-correct">0</div>
            <div class="rs-label">Correct</div>
          </div>
          <div class="result-stat">
            <div class="rs-value" id="stat-wrong">0</div>
            <div class="rs-label">Wrong</div>
          </div>
          <div class="result-stat">
            <div class="rs-value" id="stat-time">0s</div>
            <div class="rs-label">Time Taken</div>
          </div>
        </div>
        <p id="best-score-text"></p>
        <button id="retry-btn">Try Again</button>
      </div>

    </div>

    <script src="script.js"></script>
  </body>
</html>

Solution

// Step 1 — questions data
const questions = [
  {
    question: "Which keyword is used to declare a block-scoped variable in modern JavaScript?",
    options: ["var", "let", "define", "scope"],
    correct: 1
  },
  {
    question: "What does the === operator check?",
    options: [
      "Value only",
      "Type only",
      "Value and type",
      "Neither value nor type"
    ],
    correct: 2
  },
  {
    question: "What will typeof null return?",
    options: ["null", "undefined", "object", "string"],
    correct: 2
  },
  {
    question: "Which array method creates a new array with transformed values?",
    options: ["filter()", "forEach()", "reduce()", "map()"],
    correct: 3
  },
  {
    question: "What is a closure in JavaScript?",
    options: [
      "A way to close a browser window",
      "A function that remembers variables from its outer scope",
      "A method to end a loop",
      "A CSS technique"
    ],
    correct: 1
  },
  {
    question: "What does the spread operator ... do to an array?",
    options: [
      "Deletes all elements",
      "Reverses the array",
      "Expands it into individual values",
      "Sorts the array"
    ],
    correct: 2
  },
  {
    question: "Which method removes and returns the last element of an array?",
    options: ["shift()", "pop()", "splice()", "slice()"],
    correct: 1
  },
  {
    question: "What is the output of: Boolean([])?",
    options: ["false", "true", "undefined", "null"],
    correct: 1
  },
  {
    question: "Which of these is NOT a JavaScript data type?",
    options: ["Symbol", "BigInt", "Float", "Undefined"],
    correct: 2
  },
  {
    question: "What does async/await help you do?",
    options: [
      "Write synchronous code faster",
      "Write asynchronous code that reads like synchronous code",
      "Create new threads in JavaScript",
      "Speed up the event loop"
    ],
    correct: 1
  }
];

// Step 2 — select DOM elements
const welcomeScreen = document.querySelector("#welcome-screen");
const quizScreen = document.querySelector("#quiz-screen");
const resultsScreen = document.querySelector("#results-screen");

const questionNumber = document.querySelector("#question-number");
const questionText = document.querySelector("#question-text");
const optionsContainer = document.querySelector("#options-container");
const feedback = document.querySelector("#feedback");
const nextBtn = document.querySelector("#next-btn");
const timerEl = document.querySelector("#timer");
const progressFill = document.querySelector("#progress-fill");

// Step 3 — quiz state
let currentIndex = 0;
let score = 0;
let answered = false;
let timerInterval = null;
let timeLeft = 30;
let startTime = null;

// Step 4 — start button
document.querySelector("#start-btn").addEventListener("click", startQuiz);
document.querySelector("#retry-btn").addEventListener("click", startQuiz);
document.querySelector("#next-btn").addEventListener("click", nextQuestion);

function startQuiz() {
  // reset state
  currentIndex = 0;
  score = 0;
  answered = false;
  startTime = Date.now();

  // show quiz screen
  welcomeScreen.style.display = "none";
  resultsScreen.style.display = "none";
  quizScreen.style.display = "block";

  loadQuestion();
}

// Step 5 — load the current question
function loadQuestion() {
  // reset state for this question
  answered = false;
  timeLeft = 30;
  feedback.textContent = "";
  feedback.className = "";
  nextBtn.style.display = "none";

  const q = questions[currentIndex];

  // update header
  questionNumber.textContent = `Question ${currentIndex + 1} of ${questions.length}`;

  // update progress bar
  const progress = (currentIndex / questions.length) * 100;
  progressFill.style.width = `${progress}%`;

  // render question text
  questionText.textContent = q.question;

  // render options
  const letters = ["A", "B", "C", "D"];
  optionsContainer.innerHTML = q.options
    .map(
      (option, index) => `
        <button class="option-btn" data-index="${index}">
          <span class="option-letter">${letters[index]}</span>
          ${option}
        </button>
      `
    )
    .join("");

  // Step 6 — attach click listeners using event delegation
  optionsContainer.addEventListener("click", handleOptionClick, { once: true });
  // { once: true } automatically removes the listener after first click

  // Step 7 — start the countdown timer
  startTimer();
}

// Step 8 — handle option click
function handleOptionClick(e) {
  const btn = e.target.closest(".option-btn");
  if (!btn || answered) return;

  const selectedIndex = Number(btn.dataset.index);
  checkAnswer(selectedIndex);
}

// Step 9 — check the selected answer
function checkAnswer(selectedIndex) {
  if (answered) return;
  answered = true;

  // stop the timer
  clearInterval(timerInterval);

  const correct = questions[currentIndex].correct;
  const allOptions = optionsContainer.querySelectorAll(".option-btn");

  // disable all options
  allOptions.forEach((btn) => btn.disabled = true);

  // highlight correct answer always
  allOptions[correct].classList.add("correct");

  if (selectedIndex === correct) {
    // correct answer selected
    score++;
    feedback.textContent = "✅ Correct!";
    feedback.className = "correct";
  } else if (selectedIndex === -1) {
    // timer ran out — no selection
    feedback.textContent = "⏱ Time's up! Here is the correct answer.";
    feedback.className = "timeout";
  } else {
    // wrong answer selected
    allOptions[selectedIndex].classList.add("wrong");
    feedback.textContent = "❌ Wrong answer.";
    feedback.className = "wrong";
  }

  // show the next button
  nextBtn.style.display = "block";
  nextBtn.textContent =
    currentIndex === questions.length - 1 ? "See Results →" : "Next Question →";
}

// Step 10 — move to next question or show results
function nextQuestion() {
  currentIndex++;

  if (currentIndex >= questions.length) {
    showResults();
  } else {
    loadQuestion();
  }
}

// Step 11 — countdown timer
function startTimer() {
  timerEl.textContent = `⏱ ${timeLeft}s`;
  timerEl.className = "timer";

  timerInterval = setInterval(() => {
    timeLeft--;
    timerEl.textContent = `⏱ ${timeLeft}s`;

    // warning state at 10 seconds
    if (timeLeft <= 10) timerEl.className = "timer warning";

    // danger state at 5 seconds
    if (timeLeft <= 5) timerEl.className = "timer danger";

    // time ran out
    if (timeLeft <= 0) {
      clearInterval(timerInterval);
      checkAnswer(-1); // -1 means no answer selected
    }
  }, 1000);
}

// Step 12 — show results screen
function showResults() {
  quizScreen.style.display = "none";
  resultsScreen.style.display = "block";

  const total = questions.length;
  const wrong = total - score;
  const percentage = Math.round((score / total) * 100);
  const timeTaken = Math.round((Date.now() - startTime) / 1000);

  // score display
  document.querySelector("#score-number").textContent = `${score}/${total}`;

  // performance message
  let message = "";
  if (percentage === 100) message = "Perfect score! You're a JavaScript master! 🏆";
  else if (percentage >= 80) message = "Excellent work! You know your JavaScript. 🌟";
  else if (percentage >= 60) message = "Good job! A bit more practice and you'll ace it. 💪";
  else if (percentage >= 40) message = "Keep studying! You're on your way. 📚";
  else message = "Don't give up! Review the topics and try again. 🔄";

  document.querySelector("#results-title").textContent =
    percentage >= 60 ? "Great job! 🎉" : "Quiz Complete";
  document.querySelector("#performance-msg").textContent = message;

  // stat cards
  document.querySelector("#stat-correct").textContent = score;
  document.querySelector("#stat-wrong").textContent = wrong;
  document.querySelector("#stat-time").textContent = `${timeTaken}s`;

  // Step 13 — save best score to localStorage
  const bestScore = Number(localStorage.getItem("quizBestScore") || 0);

  if (score > bestScore) {
    localStorage.setItem("quizBestScore", score);
    document.querySelector("#best-score-text").textContent =
      `🏆 New best score: ${score}/${total}!`;
  } else {
    document.querySelector("#best-score-text").textContent =
      `Best score: ${bestScore}/${total}`;
  }
}

Code Execution

Trace through answering question 1 correctly (let at index 1):

StepCodeResult
Load questionquestions[0]first question rendered
Start timersetInterval(...)30s countdown starts
Click option BselectedIndex = 1checkAnswer(1) called
Stop timerclearInterval(timerInterval)timer stopped at e.g. 24s
CheckselectedIndex === correct1 === 1true
Scorescore++score = 1
Feedback"✅ Correct!"shown in green
HighlightallOptions[1].classList.add("correct")option B turns green
Next btnnextBtn.style.display = "block"Next button appears

Trace through timer running out:

StepCodeResult
Timer firestimeLeft-- → 0checkAnswer(-1)
selectedIndex === -1no option highlighted as wrongonly correct shown
Feedback"⏱ Time's up!"timeout message
Scorenot incrementedstays same

Trace through results with score 7/10:

StepCodeResult
PercentageMath.round(7/10 * 100)70
Messagepercentage >= 60"Good job! A bit more practice..."
Time taken(Date.now() - startTime) / 1000e.g. "127s"
Best score7 > bestScoresave 7 to localStorage

Output

Start quiz           → Question 1 of 10 shown, 30s timer starts
Select correct       → Green highlight, "✅ Correct!", score +1
Select wrong         → Red on chosen, green on correct, "❌ Wrong answer."
Timer runs out       → "⏱ Time's up!", correct answer revealed
Click Next           → Next question loads, timer resets to 30s
After 10 questions   → Results screen
Score 7/10           → "Good job! 70%"
New best score       → "🏆 New best score: 7/10!" saved to localStorage
Click Try Again      → Quiz resets to question 1

On this page