
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
- Head to codesandbox.io and create a new project using the JavaScript template. This gives you three files to start:
index.html,index.mjs, andstyles.css.
- Open
index.htmland take a look. You'll notice there's already a<div id="app"></div>in the body. That singledivis 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 onedivusing JavaScript!
Step 2: Store the quiz questions in a JavaScript data structure
- 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 inindex.mjsis grab that div and save it to a variable.
const appDiv = document.getElementById("app");
- 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.
- Create your
questionsarray:
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!
- 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:
questions.forEach((question, i) => { ... })→ This loops over every question in the array.questionis the current question object.iis its index (0, 1, or 2). We'll useito give every element a unique ID.
const options = question.options→ We pull out theoptionsarray from the current question and save it to a variable. This is just for convenience! It's shorter to writeoptions[0]thanquestion.options[0]four times in a row.
- We use a template literal (the backtick string) to write out the full HTML for one question card. We drop
question.questionin for the question text, useoptions[0]throughoptions[3]to fill in each button, and useito give every element a unique ID likequestion-0,options-1,feedback-2.
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
forEachloops over questions, giving usquestionandi.
querySelectorAllgrabs all buttons for that specific question by targeting#options-0 .option, then#options-1 .option, etc.
- The inner
forEachloops over those buttons and attaches a click event listener to each one.
- The event listener callback takes
eventas a parameter. That's the click event object, andevent.targetwill be the specific button that was clicked!
- For now, we’ll log the
textContentof 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:
- Figure out which button was clicked
- Compare it to the correct answer
- 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.targetis the button that was clicked..textContentgives us the text inside that button or the answer the user chose. We compare that string toquestion.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:
- The user can keep clicking buttons after they've already answered.
- 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 incrementscore. Because it lives at the top of the file, it remembers its value between clicks. It doesn't reset!
button.disabled = trueis a built-in property of HTML button elements. Setting it totruemakes 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: isansweredCountequal toquestions.length? If so, every question has been answered and it's time to show the score. We use+=onappDiv.innerHTMLto 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.
- Style the
bodyto set a background color and a font family. Consider using flexbox to center everything in a column with some padding around the edges.
- Style the elements with the
.question-cardclass 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.
- Style the
.questionclass to make the question text stand out. Try making it bigger and bold.
- Style the
.optionsclass to control how the buttons are laid out. Flexbox withflex-direction: columnwill stack them on top of each other with a gap between each one.
- Style the
.optionclass to make the buttons look like buttons! Give them a background color, some padding, rounded corners, and setcursor: pointerso the cursor turns into a hand on hover.
- Style
.option:hoverto change the button's appearance when the mouse moves over it. A slightly darker version of your button color works great here!
- Style
.option:disabledto make answered buttons look visually “locked.” A gray background andcursor: not-allowedsignals clearly to the user that they can't click anymore.
- Style the
.feedbackclass to make the answer message readable. Consider adding amin-heightso the card doesn't visually jump in size when the feedback text appears.
- Style the
.scoreclass 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-cardto 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! 🥳