Skip to content
Happy Programming Guide
Start learning
Programming Projects

JavaScript Project: Build a Quiz App

A multiple-choice quiz in the browser with scoring and a results screen. Teaches managing state, rendering screens, and keeping answer data separate from display code.

A quiz app is the best small project for learning state — the idea that your app has a current situation, and the screen is drawn from it.

The HTML#

HTML
<main class="quiz">
  <div id="progress"></div>
  <h2 id="question"></h2>
  <div id="answers"></div>
  <button id="next" hidden>Next</button>
</main>

<script src="quiz.js" defer></script>

Step 1: questions as data#

JavaScript
const QUESTIONS = [
  {
    question: "What does a variable do?",
    answers: [
      "Stores a value for later use",
      "Deletes a file",
      "Slows the program down",
      "Prints to the screen",
    ],
    correct: 0,
  },
  {
    question: "Which keyword defines a function in Python?",
    answers: ["func", "def", "function", "define"],
    correct: 1,
  },
  {
    question: "What does === check in JavaScript?",
    answers: [
      "Value only",
      "Value and type",
      "Nothing useful",
      "Whether a variable exists",
    ],
    correct: 1,
  },
];

Keeping questions as plain data means you can add fifty more without touching a line of display code. That separation is the single most valuable habit in this project.

Step 2: the state#

JavaScript
let current = 0;
let score = 0;
let answered = false;

const progressEl = document.querySelector("#progress");
const questionEl = document.querySelector("#question");
const answersEl = document.querySelector("#answers");
const nextBtn = document.querySelector("#next");

Three variables describe the entire app at any moment. Everything on screen is derived from them.

Step 3: rendering a question#

JavaScript
function renderQuestion() {
  answered = false;
  nextBtn.hidden = true;

  const q = QUESTIONS[current];

  progressEl.textContent = `Question ${current + 1} of ${QUESTIONS.length}`;
  questionEl.textContent = q.question;

  answersEl.innerHTML = "";
  q.answers.forEach((text, index) => {
    const button = document.createElement("button");
    button.className = "answer";
    button.textContent = text;
    button.dataset.index = index;
    answersEl.appendChild(button);
  });
}

Step 4: checking the answer#

JavaScript
answersEl.addEventListener("click", (event) => {
  const button = event.target.closest(".answer");
  if (!button || answered) return;      // ignore double clicks

  answered = true;
  const chosen = Number(button.dataset.index);
  const correct = QUESTIONS[current].correct;

  if (chosen === correct) score++;

  for (const b of answersEl.children) {
    const index = Number(b.dataset.index);
    if (index === correct) b.classList.add("correct");
    else if (index === chosen) b.classList.add("wrong");
    b.disabled = true;
  }

  nextBtn.hidden = false;
  nextBtn.textContent = current === QUESTIONS.length - 1 ? "See results" : "Next";
});

The answered flag stops a fast clicker scoring twice on one question — a bug that is easy to miss and obvious once someone finds it.

Step 5: moving on and finishing#

JavaScript
nextBtn.addEventListener("click", () => {
  current++;
  if (current < QUESTIONS.length) {
    renderQuestion();
  } else {
    renderResults();
  }
});

function renderResults() {
  const percent = Math.round((score / QUESTIONS.length) * 100);

  progressEl.textContent = "Finished";
  questionEl.textContent = `You scored ${score} out of ${QUESTIONS.length} (${percent}%)`;
  answersEl.innerHTML = "";
  nextBtn.hidden = true;

  const again = document.createElement("button");
  again.textContent = "Try again";
  again.addEventListener("click", () => {
    current = 0;
    score = 0;
    renderQuestion();
  });
  answersEl.appendChild(again);
}

renderQuestion();

The CSS that makes feedback obvious#

CSS
.answer {
  display: block;
  width: 100%;
  text-align: left;
  padding: 12px 16px;
  margin-bottom: 8px;
  border: 1px solid #ddd;
  border-radius: 10px;
  background: #fff;
  cursor: pointer;
  font: inherit;
}

.answer:hover:not(:disabled) { border-color: #4f46e5; }
.answer.correct { background: #dcfce7; border-color: #16a34a; }
.answer.wrong   { background: #fee2e2; border-color: #dc2626; }
.answer:disabled { cursor: default; }

Colour alone is not enough for colour-blind users — adding a tick or cross character to the correct and wrong buttons would make this properly accessible.

Questions people ask#

How do I add a lot more questions?

Move them into a separate questions.json file and fetch it at startup. None of the display code needs to change, which is the reward for keeping them separate.

Can I stop people cheating by opening DevTools?

Not in the browser — anything sent to the page can be read. Real quizzes check answers on a server.

Why use one listener on the container?

Because the answer buttons are recreated for every question. A listener on the container survives; listeners on individual buttons would need reattaching each time.

Where to go next#

Next projectBuild a portfolio page with HTML and CSS

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *