Build a dynamic quiz application with score tracking, timer functionality, and interactive feedback. Learn advanced JavaScript concepts while creating an engaging user experience.
Interactive quiz games are perfect projects for learning advanced JavaScript concepts. They require complex state management, event handling, dynamic DOM manipulation, and user interface design - all essential skills for modern web development.
In this project, you'll build a professional-quality quiz application that includes multiple question types, score tracking, time limits, and engaging user feedback. This project demonstrates many real-world programming patterns you'll use in larger applications.
By the end of this lesson, you'll have created a fully functional quiz game that you can customize, extend, and showcase in your portfolio.
A well-structured quiz application requires thoughtful data organization and clear separation of concerns.
Core Data Structures:
Question Object Model:
1const questionTypes = {
2 MULTIPLE_CHOICE: 'multiple_choice',
3 TRUE_FALSE: 'true_false',
4 FILL_IN_BLANK: 'fill_in_blank'
5};
6
7const sampleQuestion = {
8 id: 1,
9 type: questionTypes.MULTIPLE_CHOICE,
10 question: "What is the correct way to declare a variable in modern JavaScript?",
11 options: ["var x = 1", "let x = 1", "variable x = 1", "x = 1"],
12 correctAnswer: 1, // Index of correct option
13 explanation: "let is the modern way to declare variables with block scope",
14 difficulty: "easy",
15 category: "JavaScript Basics",
16 points: 10
17};Quiz State Management:
1class QuizState {
2 constructor(questions) {
3 this.questions = questions;
4 this.currentQuestionIndex = 0;
5 this.score = 0;
6 this.userAnswers = [];
7 this.timeRemaining = 0;
8 this.quizStartTime = null;
9 this.isActive = false;
10 this.isComplete = false;
11 }
12
13 getCurrentQuestion() {
14 return this.questions[this.currentQuestionIndex];
15 }
16
17 submitAnswer(answer) {
18 const question = this.getCurrentQuestion();
19 const isCorrect = this.checkAnswer(question, answer);
20
21 this.userAnswers.push({
22 questionId: question.id,
23 answer: answer,
24 isCorrect: isCorrect,
25 timeSpent: this.getTimeSpent()
26 });
27
28 if (isCorrect) {
29 this.score += question.points;
30 }
31
32 return isCorrect;
33 }
34
35 nextQuestion() {
36 if (this.currentQuestionIndex < this.questions.length - 1) {
37 this.currentQuestionIndex++;
38 return true;
39 } else {
40 this.completeQuiz();
41 return false;
42 }
43 }
44}Application Architecture:
1// Main Quiz Application
2class QuizApp {
3 constructor(containerId, questions, options = {}) {
4 this.container = document.getElementById(containerId);
5 this.state = new QuizState(questions);
6 this.options = {
7 timeLimit: 300, // 5 minutes default
8 showExplanations: true,
9 allowReview: true,
10 ...options
11 };
12
13 this.ui = new QuizUI(this.container);
14 this.timer = new QuizTimer(this.options.timeLimit);
15
16 this.init();
17 }
18
19 init() {
20 this.setupEventListeners();
21 this.ui.render('start-screen');
22 }
23
24 start() {
25 this.state.isActive = true;
26 this.state.quizStartTime = Date.now();
27 this.timer.start(this.onTimeUp.bind(this));
28 this.displayQuestion();
29 }
30}This architecture separates data management, business logic, and presentation, making the code maintainable and testable.
Creating a responsive and engaging user interface requires careful HTML structure and JavaScript manipulation.
HTML Template Structure:
1<div id="quiz-container">
2 <!-- Start Screen -->
3 <div id="start-screen" class="screen active">
4 <h1>JavaScript Quiz Challenge</h1>
5 <div class="quiz-info">
6 <p>Test your JavaScript knowledge</p>
7 <ul>
8 <li>20 questions</li>
9 <li>5 minutes time limit</li>
10 <li>Multiple choice and true/false</li>
11 </ul>
12 </div>
13 <button id="start-btn" class="btn-primary">Start Quiz</button>
14 </div>
15
16 <!-- Quiz Screen -->
17 <div id="quiz-screen" class="screen">
18 <div class="quiz-header">
19 <div class="progress-bar">
20 <div class="progress-fill"></div>
21 </div>
22 <div class="quiz-meta">
23 <span class="question-counter">1 of 20</span>
24 <span class="timer">05:00</span>
25 <span class="score">Score: 0</span>
26 </div>
27 </div>
28
29 <div class="question-container">
30 <h2 id="question-text"></h2>
31 <div id="options-container" class="options"></div>
32 <button id="next-btn" class="btn-primary" disabled>Next Question</button>
33 </div>
34 </div>
35
36 <!-- Results Screen -->
37 <div id="results-screen" class="screen">
38 <div class="results-content">
39 <h2>Quiz Complete!</h2>
40 <div class="score-display">
41 <span class="final-score">0</span>
42 <span class="max-score">/ 200</span>
43 </div>
44 <div class="results-details"></div>
45 <button id="restart-btn" class="btn-primary">Take Again</button>
46 </div>
47 </div>
48</div>Question Rendering System:
1class QuizUI {
2 constructor(container) {
3 this.container = container;
4 this.currentScreen = null;
5 }
6
7 renderQuestion(question, questionNumber, totalQuestions) {
8 const questionContainer = document.getElementById('question-text');
9 const optionsContainer = document.getElementById('options-container');
10
11 // Update question text
12 questionContainer.textContent = question.question;
13
14 // Update progress
15 this.updateProgress(questionNumber, totalQuestions);
16
17 // Clear previous options
18 optionsContainer.innerHTML = '';
19
20 // Render options based on question type
21 switch (question.type) {
22 case 'multiple_choice':
23 this.renderMultipleChoice(question, optionsContainer);
24 break;
25 case 'true_false':
26 this.renderTrueFalse(optionsContainer);
27 break;
28 case 'fill_in_blank':
29 this.renderFillInBlank(optionsContainer);
30 break;
31 }
32 }
33
34 renderMultipleChoice(question, container) {
35 question.options.forEach((option, index) => {
36 const optionElement = document.createElement('div');
37 optionElement.className = 'option';
38 optionElement.innerHTML = `
39 <input type="radio" name="answer" value="${index}" id="option-${index}">
40 <label for="option-${index}" class="option-label">
41 <span class="option-letter">${String.fromCharCode(65 + index)}</span>
42 <span class="option-text">${option}</span>
43 </label>
44 `;
45
46 container.appendChild(optionElement);
47 });
48
49 // Add event listeners for option selection
50 const radioButtons = container.querySelectorAll('input[type="radio"]');
51 radioButtons.forEach(radio => {
52 radio.addEventListener('change', this.onOptionSelect.bind(this));
53 });
54 }
55
56 onOptionSelect(event) {
57 // Enable next button
58 document.getElementById('next-btn').disabled = false;
59
60 // Add visual feedback
61 document.querySelectorAll('.option').forEach(opt => {
62 opt.classList.remove('selected');
63 });
64 event.target.closest('.option').classList.add('selected');
65 }
66
67 showFeedback(isCorrect, explanation) {
68 const feedbackEl = document.createElement('div');
69 feedbackEl.className = `feedback ${isCorrect ? 'correct' : 'incorrect'}`;
70 feedbackEl.innerHTML = `
71 <div class="feedback-icon">
72 ${isCorrect ? '✓' : '✗'}
73 </div>
74 <div class="feedback-text">
75 <strong>${isCorrect ? 'Correct!' : 'Incorrect'}</strong>
76 <p>${explanation}</p>
77 </div>
78 `;
79
80 document.querySelector('.question-container').appendChild(feedbackEl);
81
82 // Auto-advance after showing feedback
83 setTimeout(() => {
84 feedbackEl.remove();
85 this.nextQuestion();
86 }, 2000);
87 }
88}CSS Animations for Smooth Transitions:
1.screen {
2 display: none;
3 opacity: 0;
4 transform: translateY(20px);
5 transition: all 0.3s ease-in-out;
6}
7
8.screen.active {
9 display: block;
10 opacity: 1;
11 transform: translateY(0);
12}
13
14.option {
15 transition: all 0.2s ease;
16 cursor: pointer;
17}
18
19.option:hover {
20 background-color: var(--hover-color);
21 transform: translateX(5px);
22}
23
24.option.selected {
25 background-color: var(--primary-color);
26 color: white;
27}
28
29.feedback {
30 animation: slideIn 0.3s ease-out;
31}
32
33@keyframes slideIn {
34 from { opacity: 0; transform: translateY(-20px); }
35 to { opacity: 1; transform: translateY(0); }
36}This approach creates an engaging, responsive interface that provides clear feedback and smooth interactions.
Implementing robust timer functionality and score tracking adds professional polish to your quiz application.
Timer Implementation:
1class QuizTimer {
2 constructor(totalSeconds) {
3 this.totalSeconds = totalSeconds;
4 this.currentSeconds = totalSeconds;
5 this.intervalId = null;
6 this.callbacks = {
7 onTick: null,
8 onTimeUp: null,
9 onWarning: null
10 };
11 }
12
13 start(onTimeUp) {
14 this.callbacks.onTimeUp = onTimeUp;
15 this.currentSeconds = this.totalSeconds;
16
17 this.intervalId = setInterval(() => {
18 this.currentSeconds--;
19 this.updateDisplay();
20
21 // Warning at 1 minute remaining
22 if (this.currentSeconds === 60 && this.callbacks.onWarning) {
23 this.callbacks.onWarning();
24 }
25
26 // Time up
27 if (this.currentSeconds <= 0) {
28 this.stop();
29 if (this.callbacks.onTimeUp) {
30 this.callbacks.onTimeUp();
31 }
32 }
33 }, 1000);
34
35 this.updateDisplay();
36 }
37
38 stop() {
39 if (this.intervalId) {
40 clearInterval(this.intervalId);
41 this.intervalId = null;
42 }
43 }
44
45 pause() {
46 this.stop();
47 }
48
49 resume() {
50 if (!this.intervalId && this.currentSeconds > 0) {
51 this.start(this.callbacks.onTimeUp);
52 }
53 }
54
55 updateDisplay() {
56 const minutes = Math.floor(this.currentSeconds / 60);
57 const seconds = this.currentSeconds % 60;
58 const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
59
60 const timerElement = document.querySelector('.timer');
61 if (timerElement) {
62 timerElement.textContent = timeString;
63
64 // Add visual warning for low time
65 if (this.currentSeconds <= 60) {
66 timerElement.classList.add('warning');
67 }
68 }
69 }
70
71 getRemainingTime() {
72 return this.currentSeconds;
73 }
74
75 getElapsedTime() {
76 return this.totalSeconds - this.currentSeconds;
77 }
78}Advanced Score Management:
1class ScoreManager {
2 constructor() {
3 this.basePoints = 10;
4 this.timeBonus = true;
5 this.streakBonus = true;
6 this.difficultyMultipliers = {
7 easy: 1,
8 medium: 1.5,
9 hard: 2
10 };
11 }
12
13 calculateQuestionScore(question, timeSpent, isCorrect, currentStreak) {
14 if (!isCorrect) return 0;
15
16 let score = this.basePoints;
17
18 // Difficulty multiplier
19 score *= this.difficultyMultipliers[question.difficulty] || 1;
20
21 // Time bonus (faster answers get bonus points)
22 if (this.timeBonus && timeSpent < 10) {
23 const timeMultiplier = 1 + (10 - timeSpent) / 20; // Up to 1.5x bonus
24 score *= timeMultiplier;
25 }
26
27 // Streak bonus
28 if (this.streakBonus && currentStreak >= 3) {
29 score *= 1 + (currentStreak - 2) * 0.1; // 10% bonus per streak after 3
30 }
31
32 return Math.round(score);
33 }
34
35 calculateFinalScore(userAnswers, totalTime) {
36 let totalScore = 0;
37 let currentStreak = 0;
38 let maxStreak = 0;
39
40 userAnswers.forEach((answer, index) => {
41 if (answer.isCorrect) {
42 currentStreak++;
43 maxStreak = Math.max(maxStreak, currentStreak);
44
45 const questionScore = this.calculateQuestionScore(
46 answer.question,
47 answer.timeSpent,
48 true,
49 currentStreak
50 );
51 totalScore += questionScore;
52 } else {
53 currentStreak = 0;
54 }
55 });
56
57 // Final time bonus for completing quickly
58 const averageTimePerQuestion = totalTime / userAnswers.length;
59 if (averageTimePerQuestion < 15) { // Under 15 seconds average
60 totalScore *= 1.2;
61 }
62
63 return {
64 totalScore: Math.round(totalScore),
65 maxStreak,
66 averageTime: averageTimePerQuestion,
67 accuracy: (userAnswers.filter(a => a.isCorrect).length / userAnswers.length) * 100
68 };
69 }
70}Progress Tracking:
1class ProgressTracker {
2 constructor(totalQuestions) {
3 this.totalQuestions = totalQuestions;
4 this.currentQuestion = 0;
5 this.progressBar = document.querySelector('.progress-fill');
6 this.questionCounter = document.querySelector('.question-counter');
7 }
8
9 updateProgress(questionNumber) {
10 this.currentQuestion = questionNumber;
11 const percentage = (questionNumber / this.totalQuestions) * 100;
12
13 // Update progress bar
14 if (this.progressBar) {
15 this.progressBar.style.width = `${percentage}%`;
16 }
17
18 // Update counter
19 if (this.questionCounter) {
20 this.questionCounter.textContent = `${questionNumber} of ${this.totalQuestions}`;
21 }
22 }
23
24 getProgress() {
25 return {
26 current: this.currentQuestion,
27 total: this.totalQuestions,
28 percentage: (this.currentQuestion / this.totalQuestions) * 100
29 };
30 }
31}Local Storage for High Scores:
1class HighScoreManager {
2 constructor(quizId) {
3 this.storageKey = `quiz-scores-${quizId}`;
4 this.maxScores = 10;
5 }
6
7 saveScore(playerName, score, accuracy, completionTime) {
8 const scores = this.getScores();
9 const newScore = {
10 id: Date.now(),
11 playerName,
12 score,
13 accuracy,
14 completionTime,
15 date: new Date().toISOString()
16 };
17
18 scores.push(newScore);
19 scores.sort((a, b) => b.score - a.score); // Sort by score descending
20 scores.splice(this.maxScores); // Keep only top scores
21
22 localStorage.setItem(this.storageKey, JSON.stringify(scores));
23 return newScore;
24 }
25
26 getScores() {
27 try {
28 return JSON.parse(localStorage.getItem(this.storageKey)) || [];
29 } catch {
30 return [];
31 }
32 }
33
34 isHighScore(score) {
35 const scores = this.getScores();
36 return scores.length < this.maxScores || score > scores[scores.length - 1].score;
37 }
38}These systems work together to create a comprehensive, engaging quiz experience with professional features.
Creating smooth user interactions requires careful event handling and state management throughout the quiz experience.
Comprehensive Event Management:
1class QuizEventManager {
2 constructor(quizApp) {
3 this.quiz = quizApp;
4 this.keyboardEnabled = true;
5 this.setupEventListeners();
6 }
7
8 setupEventListeners() {
9 // Start quiz
10 document.getElementById('start-btn')?.addEventListener('click', () => {
11 this.quiz.start();
12 });
13
14 // Answer selection
15 document.addEventListener('change', (e) => {
16 if (e.target.name === 'answer') {
17 this.handleAnswerSelection(e.target.value);
18 }
19 });
20
21 // Next question
22 document.getElementById('next-btn')?.addEventListener('click', () => {
23 this.handleNextQuestion();
24 });
25
26 // Keyboard shortcuts
27 document.addEventListener('keydown', (e) => {
28 if (!this.keyboardEnabled) return;
29
30 switch (e.key) {
31 case '1':
32 case '2':
33 case '3':
34 case '4':
35 if (this.quiz.state.isActive) {
36 this.selectOption(parseInt(e.key) - 1);
37 }
38 break;
39 case 'Enter':
40 if (!document.getElementById('next-btn').disabled) {
41 this.handleNextQuestion();
42 }
43 break;
44 case ' ':
45 e.preventDefault();
46 if (this.quiz.state.isActive) {
47 this.handleNextQuestion();
48 }
49 break;
50 }
51 });
52
53 // Prevent page refresh during quiz
54 window.addEventListener('beforeunload', (e) => {
55 if (this.quiz.state.isActive) {
56 e.preventDefault();
57 e.returnValue = 'Are you sure you want to leave? Your progress will be lost.';
58 }
59 });
60
61 // Handle visibility changes (tab switching)
62 document.addEventListener('visibilitychange', () => {
63 if (document.hidden && this.quiz.state.isActive) {
64 this.quiz.timer.pause();
65 this.showWarning('Quiz paused - return to continue');
66 } else if (!document.hidden && this.quiz.state.isActive) {
67 this.quiz.timer.resume();
68 this.hideWarning();
69 }
70 });
71 }
72
73 selectOption(index) {
74 const radioButton = document.querySelector(`input[value="${index}"]`);
75 if (radioButton) {
76 radioButton.checked = true;
77 radioButton.dispatchEvent(new Event('change'));
78 }
79 }
80
81 handleAnswerSelection(answerIndex) {
82 // Store selected answer
83 this.quiz.selectedAnswer = parseInt(answerIndex);
84
85 // Enable next button
86 const nextBtn = document.getElementById('next-btn');
87 nextBtn.disabled = false;
88
89 // Add visual feedback
90 this.highlightSelectedOption(answerIndex);
91
92 // Auto-advance option (can be configured)
93 if (this.quiz.options.autoAdvance) {
94 setTimeout(() => {
95 this.handleNextQuestion();
96 }, 1000);
97 }
98 }
99
100 highlightSelectedOption(selectedIndex) {
101 document.querySelectorAll('.option').forEach((option, index) => {
102 option.classList.remove('selected', 'correct', 'incorrect');
103
104 if (index === selectedIndex) {
105 option.classList.add('selected');
106 }
107 });
108 }
109
110 showAnswerFeedback(isCorrect, correctIndex, explanation) {
111 this.keyboardEnabled = false;
112
113 document.querySelectorAll('.option').forEach((option, index) => {
114 if (index === correctIndex) {
115 option.classList.add('correct');
116 } else if (index === this.quiz.selectedAnswer && !isCorrect) {
117 option.classList.add('incorrect');
118 }
119 });
120
121 // Show explanation
122 const feedbackEl = this.createFeedbackElement(isCorrect, explanation);
123 document.querySelector('.question-container').appendChild(feedbackEl);
124
125 // Re-enable keyboard after feedback
126 setTimeout(() => {
127 this.keyboardEnabled = true;
128 feedbackEl.remove();
129 }, 3000);
130 }
131}Accessibility and User Experience:
1class AccessibilityManager {
2 constructor() {
3 this.setupAccessibility();
4 }
5
6 setupAccessibility() {
7 // Add ARIA labels
8 this.addAriaLabels();
9
10 // Focus management
11 this.setupFocusManagement();
12
13 // Screen reader announcements
14 this.setupScreenReaderSupport();
15 }
16
17 addAriaLabels() {
18 const elements = {
19 'quiz-container': 'Quiz Application',
20 'question-text': 'Current Question',
21 'options-container': 'Answer Options',
22 'timer': 'Time Remaining',
23 'progress-bar': 'Quiz Progress'
24 };
25
26 Object.entries(elements).forEach(([id, label]) => {
27 const element = document.getElementById(id);
28 if (element) {
29 element.setAttribute('aria-label', label);
30 }
31 });
32 }
33
34 announceToScreenReader(message) {
35 const announcement = document.createElement('div');
36 announcement.setAttribute('aria-live', 'polite');
37 announcement.setAttribute('aria-atomic', 'true');
38 announcement.className = 'sr-only';
39 announcement.textContent = message;
40
41 document.body.appendChild(announcement);
42
43 setTimeout(() => {
44 document.body.removeChild(announcement);
45 }, 1000);
46 }
47
48 focusOnCurrentQuestion() {
49 const questionElement = document.getElementById('question-text');
50 if (questionElement) {
51 questionElement.focus();
52 }
53 }
54}Touch and Mobile Support:
1class TouchHandler {
2 constructor() {
3 this.touchStartY = null;
4 this.touchStartX = null;
5 this.setupTouchEvents();
6 }
7
8 setupTouchEvents() {
9 document.addEventListener('touchstart', (e) => {
10 this.touchStartY = e.touches[0].clientY;
11 this.touchStartX = e.touches[0].clientX;
12 });
13
14 document.addEventListener('touchend', (e) => {
15 if (!this.touchStartY || !this.touchStartX) return;
16
17 const touchEndY = e.changedTouches[0].clientY;
18 const touchEndX = e.changedTouches[0].clientX;
19
20 const deltaY = this.touchStartY - touchEndY;
21 const deltaX = this.touchStartX - touchEndX;
22
23 // Swipe gestures for navigation
24 if (Math.abs(deltaX) > Math.abs(deltaY)) {
25 if (deltaX > 50) {
26 // Swipe left - next question
27 this.handleSwipeLeft();
28 } else if (deltaX < -50) {
29 // Swipe right - previous question (if allowed)
30 this.handleSwipeRight();
31 }
32 }
33
34 this.touchStartY = null;
35 this.touchStartX = null;
36 });
37 }
38
39 handleSwipeLeft() {
40 if (!document.getElementById('next-btn').disabled) {
41 document.getElementById('next-btn').click();
42 }
43 }
44}This comprehensive event handling system creates an engaging, accessible, and responsive quiz experience across all devices and interaction methods.
Your progress is saved in this browser, on this device only.