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.
// 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
/* 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.
// 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
// 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();
#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.
CSS Highlights
/* 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
categoryfield 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
| Concept | Where Applied |
|---|---|
| ES6 Class | Quiz class encapsulates all game logic |
| Private Fields (#) | Prevent external state mutation |
| Fisher-Yates Shuffle | #shuffle() randomizes question order |
| Closures | Event handlers capture quiz variable |
| Event Delegation | Options grid click handler |
| Keyboard Events | Press 1β4 to select options |
| Conditional Rendering | show/hide quiz vs results screen |
| Data-driven UI | HTML 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