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" buttonLogic
- Store questions in an array — each with a question, options, and correct index
- Track quiz state — current question index, score, selected answer, timer
- Render one question at a time
- On answer select — lock all options, highlight correct and wrong
- Run a 30 second countdown timer per question
- When timer hits 0 — auto-lock and reveal the correct answer
- Show results when all questions are answered
- Save best score to localStorage
Flow
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):
| Step | Code | Result |
|---|---|---|
| Load question | questions[0] | first question rendered |
| Start timer | setInterval(...) | 30s countdown starts |
| Click option B | selectedIndex = 1 | checkAnswer(1) called |
| Stop timer | clearInterval(timerInterval) | timer stopped at e.g. 24s |
| Check | selectedIndex === correct → 1 === 1 | true |
| Score | score++ | score = 1 |
| Feedback | "✅ Correct!" | shown in green |
| Highlight | allOptions[1].classList.add("correct") | option B turns green |
| Next btn | nextBtn.style.display = "block" | Next button appears |
Trace through timer running out:
| Step | Code | Result |
|---|---|---|
| Timer fires | timeLeft-- → 0 | checkAnswer(-1) |
selectedIndex === -1 | no option highlighted as wrong | only correct shown |
| Feedback | "⏱ Time's up!" | timeout message |
| Score | not incremented | stays same |
Trace through results with score 7/10:
| Step | Code | Result |
|---|---|---|
| Percentage | Math.round(7/10 * 100) | 70 |
| Message | percentage >= 60 | "Good job! A bit more practice..." |
| Time taken | (Date.now() - startTime) / 1000 | e.g. "127s" |
| Best score | 7 > bestScore | save 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