16.2 Quiz Interface

16.2 Quiz Interface

🎯 Learning Goals

  • Use objects to create a JavaScript data structure for questions
  • Apply event listeners to handle quiz interaction
 

đź“— Technical Vocabulary

  • Objects
  • Event Listeners
notion image

Introduction to the quiz interface!

A quiz interface like the one above is a fun and user-interactive element that you can include on your website. You can assess your user’s knowledge on the topic of your site. You can also create something like a personality quiz where users input habits or preferences. While there are many ways to structure a quiz on a website, we’ll create three questions on a single HTML page for this tutorial.

Step 1: Create a new CodeSandbox project

  1. Head to codesandbox.io and create a new project using the JavaScript template. This gives you three files to start: index.html, index.mjs, and styles.css.
  1. Open index.html and take a look. You'll notice there's already a <div id="app"></div> in the body. That single div is going to be our canvas. Everything you see in the quiz (every card, every button, every feedback message) is going to get injected into that one div using JavaScript!

Step 2: Store the quiz questions in a JavaScript data structure

  1. Remember that empty <div id="app"></div> in your HTML file? Before we can add anything to it, we need to give JavaScript a way to find and control it! So the very first thing we'll do in index.mjs is grab that div and save it to a variable.
    1. const appDiv = document.getElementById("app");
  1. Now we need to organize our quiz data. Ask yourself: what does a quiz question need to have?
      • The question text itself
      • A list of answer choices
      • The correct answer
      • A message to show when the user gets it right
      • A message to show when the user gets it wrong
      That's an object! And since we have multiple questions, we need an array of objects. This structure is called an array of objects, and it's one of the most common patterns you'll see in real-world JavaScript.
  1. Create your questions array:
    1. const questions = [ { question: "What year was Kode with Klossy founded?", options: ["2000", "2010", "2015", "2020"], correctAnswer: "2015", correctMessage: "Correct! Kode with Klossy was founded in 2015.", incorrectMessage: "Not quite. Kode with Klossy was founded in 2015.", }, { question: "Which tracks are available at KWK camp in 2026?", options: [ "Data Science", "Web Development", "AI/Machine Learning", "All of the above", ], correctAnswer: "All of the above", correctMessage: "Correct! KWK is teaching Data Science, Web Dev, and AI/ML this summer!", incorrectMessage: "Incorrect. Kode with Klossy has 3 amazing learning tracks!", }, { question: "How many KWK alumni are there?", options: ["5,000", "7,000", "10,000", "12,000"], correctAnswer: "12,000", correctMessage: "There are over 12,000 alumni of KWK programs!", incorrectMessage: "Incorrect. Guess a bigger number!", }, ];
      Take a moment to read through this carefully. Notice how all three questions follow the exact same shape with the same keys. That consistency is what's going to let us loop over them in the next step and treat every question the same way!
  1. Customize it! Swap in your own questions, answers, and messages. This is your quiz — make it about something you care about.

Step 3: Build the HTML dynamically with JavaScript

Now we’ve included the question and answer data for our quiz in an array of objects, but we want the user to be able to see and interact with that data. This is where the magic happens! We're going to use innerHTML to create entire chunks of HTML, repeated for every question, using a loop.
Here's how it works: we're going to loop over the questions array. For each question, we'll build a string of HTML that represents one “question card.” That card has a paragraph for the question text, a div with four buttons (one per answer choice), and an empty paragraph where feedback will eventually appear. Then we'll stick that HTML string into appDiv.
Paste this below your questions array:
// Build all the HTML questions.forEach((question, i) => { const options = question.options; appDiv.innerHTML += ` <div class="question-card" id="question-card-${i}"> <p class="question" id="question-${i}">${question.question}</p> <div class="options" id="options-${i}"> <button class="option" id="option-${i}-0">${options[0]}</button> <button class="option" id="option-${i}-1">${options[1]}</button> <button class="option" id="option-${i}-2">${options[2]}</button> <button class="option" id="option-${i}-3">${options[3]}</button> </div> <p class="feedback" id="feedback-${i}"></p> </div>`; });
 
Let's break down what's happening here:
  1. questions.forEach((question, i) => { ... }) → This loops over every question in the array. question is the current question object. i is its index (0, 1, or 2). We'll use i to give every element a unique ID.
  1. const options = question.options → We pull out the options array from the current question and save it to a variable. This is just for convenience! It's shorter to write options[0] than question.options[0] four times in a row.
  1. We use a template literal (the backtick string) to write out the full HTML for one question card. We drop question.question in for the question text, use options[0] through options[3] to fill in each button, and use i to give every element a unique ID like question-0, options-1, feedback-2.
  1. appDiv.innerHTML += → The += means “add this to whatever's already there.” So the first time through the loop, we add question 0's card. Second time, question 1's card gets appended. Third time, question 2. By the end of the loop, all three cards are on the page!
Save your file and check the preview. You should see three question cards! They aren’t styled yet, but the questions and buttons should all be there! That HTML was built entirely by JavaScript from your data. No HTML file was touched. Pretty powerful, right?
💡 Why give everything a unique ID? Because later, we need to find specific elements on the page, like “the feedback paragraph for question 1” or “all the buttons for question 2.” Those unique IDs are what let us pinpoint exactly the right element.

Step 4: Add event listeners to all buttons

The buttons exist on the page now, but clicking them does nothing. Time to fix that!
You already know how to add a click event listener to one element using getElementById. But now we need to add listeners to multiple buttons across multiple questions. We're going to do this with a loop and a method you haven't used yet: querySelectorAll.
document.querySelectorAll(selector) is like querySelector, except instead of returning just the first matching element, it returns all elements that match a CSS selector. So document.querySelectorAll(".option") would return every element with the class option. We can use it with any valid CSS selector, including ones that combine an ID and a class, like #options-0 .option, which means “every element with class option that's inside the element with ID options-0.”
Paste this below the HTML-building loop:
// Attach event listeners questions.forEach((question, i) => { const buttons = document.querySelectorAll(`#options-${i} .option`); buttons.forEach((button) => { button.addEventListener("click", (event) => { // (We'll fill this in next!) console.log(event.target.textContent); }); }); });
Here's what's happening:
  • The outer forEach loops over questions, giving us question and i.
  • querySelectorAll grabs all buttons for that specific question by targeting #options-0 .option, then #options-1 .option, etc.
  • The inner forEach loops over those buttons and attaches a click event listener to each one.
  • The event listener callback takes event as a parameter. That's the click event object, and event.target will be the specific button that was clicked!
  • For now, we’ll log the textContent of the button that’s clicked. Save your file and check the preview. When you click a button, do you see the button’s text logged to the console?
⚠️ Order matters! This loop needs to come after the HTML-building loop. If you try to attach event listeners to buttons that don't exist yet, JavaScript will just find nothing and silently do nothing. Build the HTML first, then add the listeners.

Step 5: Show the user feedback after they select an answer

Now we need to adjust what actually happens when a button is clicked. We need our code to:
  1. Figure out which button was clicked
  1. Compare it to the correct answer
  1. Show the right feedback message
Update the event listener callback you wrote in Step 4 to look like this:
questions.forEach((question, i) => { const buttons = document.querySelectorAll(`#options-${i} .option`); buttons.forEach((button) => { button.addEventListener("click", (event) => { // Check the answer const feedback = document.getElementById(`feedback-${i}`); if (event.target.textContent === question.correctAnswer) { feedback.textContent = question.correctMessage; } else { feedback.textContent = question.incorrectMessage; } }); }); });
Let's walk through the new lines:
  • document.getElementById('feedback-${i}') → This grabs the empty feedback paragraph we built for this question in Step 3. Remember those unique IDs? This is exactly why we made them!
  • event.target.textContent → event.target is the button that was clicked. .textContent gives us the text inside that button or the answer the user chose. We compare that string to question.correctAnswer.
    • If they match, we set the feedback paragraph's text to the correct message. If they don't, we set it to the incorrect message.
Save and test it! Click an answer. You should see a feedback message appear below the question. Try clicking the wrong answer on one, the right answer on another.

Step 6: Disable buttons and track the score

Two things are still missing from a real quiz experience:
  1. The user can keep clicking buttons after they've already answered.
  1. There's no final score at the end.
Before we can track the score, we need two variables that exist outside of any loop or callback. Go back to the top of your index.mjs file, right after the appDiv line, and add these:
let score = 0; let answeredCount = 0;
We're declaring them at the top of the file because they need to persist across all three questions. If we declared them inside the loop, they'd get reset to 0 every time the loop ran. Up at the top, they hold their value for the entire life of the quiz.
Now update the event listener callback to use them:
questions.forEach((question, i) => { const buttons = document.querySelectorAll(`#options-${i} .option`); buttons.forEach((button) => { button.addEventListener("click", (event) => { // Check the answer const feedback = document.getElementById(`feedback-${i}`); if (event.target.textContent === question.correctAnswer) { feedback.textContent = question.correctMessage; score++; } else { feedback.textContent = question.incorrectMessage; } // Disable all buttons for this question buttons.forEach((button) => { button.disabled = true; }); // Check if quiz is finished answeredCount++; if (answeredCount === questions.length) { appDiv.innerHTML += `<p class="score">You got ${score} out of ${questions.length} correct!</p>`; } }); }); });
Here’s what’s new:
  • score++ → If the answer was correct, we increment score. Because it lives at the top of the file, it remembers its value between clicks. It doesn't reset!
  • button.disabled = true is a built-in property of HTML button elements. Setting it to true makes the button unclickable. We loop over all the buttons for this question and disable every single one.
  • Every time a button is clicked, we increment answeredCount. Then we check: is answeredCount equal to questions.length? If so, every question has been answered and it's time to show the score. We use += on appDiv.innerHTML to append a final score paragraph without overwriting the question cards.
Save and test the full flow of your quiz interface! Answer all three questions and confirm the score appears at the end.

Step 7: Add styling

Your quiz works! Now let's make it look good.
Option 1: Copy these styles into your styles.css file!
body { background-color: #007052; font-family: Arial, sans-serif; padding: 40px 20px; } #app { max-width: 680px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; } .question-card { background-color: #f7f5eb; border-radius: 16px; padding: 32px; } .question { font-size: 1.1rem; font-weight: bold; color: black; margin-bottom: 24px; } .options { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; } .option { background-color: #007052; color: #e3ff00; font-size: 0.9rem; font-weight: bold; border: none; border-radius: 10px; padding: 14px 12px; cursor: pointer; text-align: center; } .option:hover { background-color: #8427c9; color: #ff99fc; } .option:disabled { background-color: #ccc; color: #888; cursor: not-allowed; } .feedback { font-size: 0.9rem; font-weight: bold; color: #007052; } .score { background-color: #e3ff00; border-radius: 16px; padding: 32px; font-size: 1.4rem; font-weight: bold; text-align: center; }
Option 2: Create your own styles using these suggestions.
  1. Style the body to set a background color and a font family. Consider using flexbox to center everything in a column with some padding around the edges.
  1. Style the elements with the .question-card class to visually group each question with its answer choices. A background color, rounded corners, padding, and a box shadow can make each card feel like its own contained unit.
  1. Style the .question class to make the question text stand out. Try making it bigger and bold.
  1. Style the .options class to control how the buttons are laid out. Flexbox with flex-direction: column will stack them on top of each other with a gap between each one.
  1. Style the .option class to make the buttons look like buttons! Give them a background color, some padding, rounded corners, and set cursor: pointer so the cursor turns into a hand on hover.
  1. Style .option:hover to change the button's appearance when the mouse moves over it. A slightly darker version of your button color works great here!
  1. Style .option:disabled to make answered buttons look visually “locked.” A gray background and cursor: not-allowed signals clearly to the user that they can't click anymore.
  1. Style the .feedback class to make the answer message readable. Consider adding a min-height so the card doesn't visually jump in size when the feedback text appears.
  1. Style the .score class to make the final result feel like a moment. Give it a larger font size, bold weight, and consider reusing the card styling from .question-card to keep things consistent.

You Did It! 🎉

The biggest shift in this project was letting JavaScript build your HTML instead of writing it by hand. This is how professional web apps work: the data and the display are kept separate, and JavaScript is the bridge between them. You now understand why that pattern exists and how to use it. That's a huge deal!
Your quiz app should be up and running, but this is just a starting point! Here are some ideas if you want to keep building:
  • Add a reset button at the end that lets the user retake the quiz
  • Show each question one at a time instead of all at once, and reveal the next question only after the current one is answered
  • Color-code the feedback (green for correct, red for incorrect)
  • Add a timer that counts down for each question
  • Swap out the buttons for a multiple choice format where users select an answer for each question and hit a submit button at the end to confirm
This quiz is yours! Make it something you’re proud of! 🥳