The plan: A timed multiple-choice quiz about JavaScript (of course!). Ten questions, fifteen seconds each, with instant feedback, a score, and a best score that's remembered.
Features:
- A start screen, a question screen, and a results screen
- Questions (and their answer choices) appear in a random order every game
- A countdown timer with a shrinking bar that turns red in the last five seconds
- Click an answer to see if you're right; the correct answer always lights up green
- Running out of time counts as a wrong answer
- A results screen with your score, a message, and your all-time best score
The data: an array of question objects. Each one has the question, the choices, and the correct answer (stored as the answer's text, so it still works after we shuffle the choices).
The HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The CurlyScript Quiz Show</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="quiz">
<!-- Screen 1: the welcome screen -->
<section id="start-screen">
<h1>🧠 The CurlyScript Quiz Show</h1>
<p>10 questions. 15 seconds each. How much JavaScript do you really know?</p>
<p id="best-score"></p>
<button id="start-btn" class="big">Start the quiz</button>
</section>
<!-- Screen 2: the questions -->
<section id="question-screen" class="hidden">
<div class="top-bar">
<span id="progress"></span>
<span id="timer"></span>
<span id="score"></span>
</div>
<div class="time-track"><div id="time-bar"></div></div>
<h2 id="question-text"></h2>
<div id="choices"></div>
<p id="feedback"></p>
<button id="next-btn" class="hidden">Next question →</button>
</section>
<!-- Screen 3: the results -->
<section id="result-screen" class="hidden">
<h1 id="result-title"></h1>
<p id="result-text"></p>
<button id="restart-btn" class="big">Play again</button>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
The page has three screens (<section> elements). Only one is visible at a time: the other two have the hidden class. The JavaScript will switch between them.
The CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: system-ui, sans-serif;
background: #22304a;
color: #22304a;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.quiz {
width: 100%;
max-width: 560px;
background: white;
border-radius: 24px;
padding: 28px;
text-align: center;
}
.hidden { display: none; }
button {
font-size: 16px;
padding: 12px 18px;
border: none;
border-radius: 14px;
cursor: pointer;
background: #22304a;
color: white;
}
button.big { font-size: 20px; padding: 14px 28px; background: #ff6b6b; }
.top-bar { display: flex; justify-content: space-between; font-weight: bold; }
.time-track { height: 8px; background: #eef1f6; border-radius: 8px; margin: 10px 0 20px; overflow: hidden; }
#time-bar { height: 100%; width: 100%; background: #2ec4b6; transition: width 1s linear; }
#time-bar.low { background: #ff6b6b; }
#choices { display: grid; gap: 10px; margin: 20px 0; }
.choice { background: #eef1f6; color: #22304a; text-align: left; font-family: monospace; font-size: 17px; }
.choice:hover:not(:disabled) { background: #fff6b8; }
.choice:disabled { cursor: default; }
.choice.correct { background: #6bcb77; color: white; }
.choice.wrong { background: #ff6b6b; color: white; }
#feedback { min-height: 24px; font-weight: bold; }
Note the .hidden class (which hides a screen), the .correct and .wrong classes for answer buttons, and the transition on the time bar, which makes it shrink smoothly instead of jumping.
The JavaScript
Part 1: The questions
// ---------- The questions ----------
const QUESTIONS = [
{ question: "What does typeof null give?", choices: ['"object"', '"null"', '"undefined"', '"number"'], answer: '"object"' },
{ question: "Which keyword makes a variable that can't be reassigned?", choices: ["const", "let", "var", "fixed"], answer: "const" },
{ question: 'What is "5" + 3?', choices: ['"53"', "8", "NaN", "an error"], answer: '"53"' },
{ question: "Which method adds an item to the end of an array?", choices: ["push()", "pop()", "shift()", "unshift()"], answer: "push()" },
{ question: "What does [1, 2, 3].map(n => n * 2) give?", choices: ["[2, 4, 6]", "[1, 2, 3]", "12", "[2]"], answer: "[2, 4, 6]" },
{ question: "What is the index of the FIRST item in an array?", choices: ["0", "1", "-1", "first"], answer: "0" },
{ question: "Which operator checks both value AND type?", choices: ["===", "==", "=", "!="], answer: "===" },
{ question: "What does DOM stand for?", choices: ["Document Object Model", "Data Over Memory", "Digital Output Machine", "Do One More"], answer: "Document Object Model" },
{ question: "Which of these values is falsy?", choices: ["0", '"0"', "[]", '"false"'], answer: "0" },
{ question: "In which year was JavaScript created?", choices: ["1995", "1985", "2005", "2015"], answer: "1995" },
];
const SECONDS_PER_QUESTION = 15;
const BEST_SCORE_KEY = "quiz-best-score";
All the questions live in one array of objects. Want to add more questions, or make a quiz about football, or world history, or your favorite cartoon? Just change this array. None of the other code needs to change. Constant settings like SECONDS_PER_QUESTION go at the top in UPPER_SNAKE_CASE (Grade 1), so they're easy to find and tweak.
Part 2: The game state
// ---------- Game state ----------
let questions = [];
let currentIndex = 0;
let score = 0;
let timeLeft = 0;
let timerId = null;
// ---------- Elements ----------
const $ = (selector) => document.querySelector(selector);
const startScreen = $("#start-screen");
const questionScreen = $("#question-screen");
const resultScreen = $("#result-screen");
const choicesBox = $("#choices");
const feedback = $("#feedback");
const nextButton = $("#next-btn");
const timeBar = $("#time-bar");
The state for this app is: which questions we're asking (in shuffled order), which one we're on, the score, the time left, and the timer's ID (so we can stop it).
The line const $ = (selector) => document.querySelector(selector); is a fun shortcut: a tiny arrow function with a one-character name, so we can write $("#score") instead of document.querySelector("#score"). (You'll see $ used like this a lot online. It's a nod to a once hugely popular library called jQuery.)
Part 3: Helpers
// ---------- Helpers ----------
function showScreen(screen) {
[startScreen, questionScreen, resultScreen].forEach((s) => s.classList.add("hidden"));
screen.classList.remove("hidden");
}
// Fisher-Yates shuffle: returns a shuffled copy of an array
function shuffle(array) {
const copy = [...array];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
function getBestScore() {
return Number(localStorage.getItem(BEST_SCORE_KEY)) || 0;
}
function showBestScore() {
const best = getBestScore();
$("#best-score").textContent = best > 0 ? `🏆 Your best score: ${best}/${QUESTIONS.length}` : "";
}
showScreenhides all three screens, then shows the one you asked for.shuffleuses a classic algorithm called the Fisher-Yates shuffle. It walks backwards through a copy of the array, swapping each item with a randomly chosen item at or before it. (That swap uses the destructuring trick from Grade 13!) It's the standard, fair way to shuffle, where every possible order is equally likely. Notice it shuffles a copy ([...array]), so the originalQUESTIONSarray is never changed.getBestScorereads the best score from storage.Number(null)is0, and|| 0guards against anything else weird.
Part 4: The timer
// ---------- The timer ----------
function startTimer() {
timeLeft = SECONDS_PER_QUESTION;
updateTimer();
timerId = setInterval(() => {
timeLeft--;
updateTimer();
if (timeLeft <= 0) {
stopTimer();
revealAnswer(null);
}
}, 1000);
}
function stopTimer() {
clearInterval(timerId);
timerId = null;
}
function updateTimer() {
$("#timer").textContent = `⏱️ ${timeLeft}s`;
timeBar.style.width = `${(timeLeft / SECONDS_PER_QUESTION) * 100}%`;
timeBar.classList.toggle("low", timeLeft <= 5);
}
setInterval (Freshman year) ticks once a second. Each tick takes one second off, updates the display, and checks for zero. When time runs out, we stop the timer and reveal the answer with null (meaning "the player didn't pick anything"). The time bar's width is set as a percentage of the time remaining, and the low class turns it red for the last five seconds.
It's very important that stopTimer() is called whenever a question ends, whether the player answered or time ran out. If we forgot, the old interval would keep ticking in the background, and when the next question started its own timer, there'd be two timers running at once, making the clock count down twice as fast. (Buggsy calls this "the double-speed disaster.")
Part 5: Playing the game
// ---------- Playing the game ----------
function startQuiz() {
questions = shuffle(QUESTIONS);
currentIndex = 0;
score = 0;
showScreen(questionScreen);
showQuestion();
}
function showQuestion() {
const q = questions[currentIndex];
$("#progress").textContent = `Question ${currentIndex + 1}/${questions.length}`;
$("#score").textContent = `Score: ${score}`;
$("#question-text").textContent = q.question;
feedback.textContent = "";
nextButton.classList.add("hidden");
choicesBox.innerHTML = "";
shuffle(q.choices).forEach((choice) => {
const button = document.createElement("button");
button.className = "choice";
button.textContent = choice;
button.addEventListener("click", () => {
stopTimer();
revealAnswer(choice);
});
choicesBox.append(button);
});
startTimer();
}
function revealAnswer(picked) {
const correct = questions[currentIndex].answer;
choicesBox.querySelectorAll(".choice").forEach((button) => {
button.disabled = true;
if (button.textContent === correct) button.classList.add("correct");
else if (button.textContent === picked) button.classList.add("wrong");
});
if (picked === correct) {
score++;
feedback.textContent = "✅ Correct! Nice one!";
} else if (picked === null) {
feedback.textContent = `⏰ Time's up! The answer was ${correct}.`;
} else {
feedback.textContent = `❌ Not quite. The answer was ${correct}.`;
}
$("#score").textContent = `Score: ${score}`;
const isLast = currentIndex === questions.length - 1;
nextButton.textContent = isLast ? "See my results 🎉" : "Next question →";
nextButton.classList.remove("hidden");
}
function nextQuestion() {
currentIndex++;
if (currentIndex < questions.length) {
showQuestion();
} else {
showResults();
}
}
function showResults() {
const total = questions.length;
const percent = Math.round((score / total) * 100);
let title;
if (percent === 100) title = "🏆 PERFECT! JavaScript genius!";
else if (percent >= 70) title = "🎉 Great job!";
else if (percent >= 40) title = "🙂 Not bad!";
else title = "📚 Time to revise!";
const best = getBestScore();
const isNewBest = score > best;
if (isNewBest) localStorage.setItem(BEST_SCORE_KEY, score);
$("#result-title").textContent = title;
$("#result-text").textContent =
`You scored ${score} out of ${total} (${percent}%).` + (isNewBest ? " That's a new best score! 🥳" : "");
showScreen(resultScreen);
}
startQuizshuffles the questions, resets the score and shows the first question.showQuestionfills in the question, then builds one button per choice (in shuffled order). Each button's click listener stops the timer and reveals the answer. Note the closure (Grade 9): each button's arrow function remembers its ownchoice.revealAnswerdisables all the buttons (no changing your mind!), colors the correct one green and the wrong pick red, updates the score and feedback, and shows the Next button. On the last question, the button says "See my results" instead.showResultsworks out a percentage and a message, checks for a new best score, saves it if needed, and switches to the results screen.
Part 6: Starting it all up
// ---------- Events ----------
$("#start-btn").addEventListener("click", startQuiz);
$("#restart-btn").addEventListener("click", startQuiz);
nextButton.addEventListener("click", nextQuestion);
showBestScore();
Three buttons, three listeners, and show the best score on the start screen. Done!
- Your own questions: rewrite the
QUESTIONSarray to make a quiz about something you love. - Speed bonus: award extra points for answering quickly (for example, 1 point plus 1 bonus point if more than 10 seconds are left).
- Review screen: at the end, show a list of the questions the player got wrong, with the correct answers.
- Keyboard controls: let the player press 1–4 to choose an answer and Enter for the next question.
- Questions from the internet: load questions from a free trivia API with
fetch. (Search for "Open Trivia Database.")