Build a Trivia Quiz App with JavaScript

Build a browser trivia game that fetches live questions from an API.

Introduction

30 Second Summary

You know that moment at a pub quiz or on a game show when you just know the answer? Building your own trivia game lets you recreate that feeling, and you get to decide the rules.

In this project, you will build a browser-based trivia quiz app using JavaScript that fetches live questions from the internet, gives instant feedback on your answers, and tracks your score across 10 rounds.

What You'll Build

You will have a trivia game you can open in any browser, click "Start Quiz," answer 10 random multiple-choice questions with real-time green/red feedback, and share your final score with friends.

By the end of this project, you'll have:

  • A working quiz game that pulls fresh questions from a live API every time you play, so no two rounds are the same.
  • Hands-on experience with JavaScript's core web patterns: selecting DOM elements, handling click events, and fetching data with async/await.
  • A Vite-powered development setup that reloads your app the instant you save a file, ready to reuse on future projects.
  • Secret Mission: A difficulty selector and category picker so you can customize your quiz before starting.

Are there any prerequisites?

You need coding experience in another language (Python, Java, C#, or similar). The concepts will transfer. No prior JavaScript knowledge is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Taking a moment to set your intention turns a follow-along exercise into something you actually remember.

Set Up Your JavaScript Development Environment

To build your trivia quiz app, you need two things: a way to run JavaScript tooling on your computer, and a development server that instantly shows your changes in the browser the moment you save a file.

Node.js is the JavaScript runtime that powers all the build tools you will use. Vite is a modern development server that refreshes your browser automatically whenever you edit code. In this step, you will install both and scaffold your project.

In this step, get ready to:
  • Install Node.js and verify it works.
  • Scaffold a new Vite project with the vanilla JavaScript template.
  • Start the Vite dev server and see the starter page in your browser.
Install and verify Node.js

Node.js lets you run JavaScript outside the browser. You need it because Vite and npm (the package manager bundled with Node.js) both depend on it.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, next to the 1 key on your keyboard).
  • Check whether Node.js is already installed by running this command:
node --version

Look at the output in your terminal and pick the tab that matches what you see.

✔️ I see version 20.19 or higher

Your terminal prints something like v24.18.0 (or any number at or above 20.19). Node.js is ready to go.

ⓧ I see an older version

If the number is below 20.19, you need a newer version. Vite requires Node.js 20.19+ to run.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the LTS installer for your operating system.
  • Run the installer and accept all defaults. It will replace your older version.
  • Close and reopen the VS Code terminal, then run node --version again to confirm it now shows 20.19 or higher.

ⓧ Command not found

Node.js is not installed yet. Let's fix that.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the LTS installer for your operating system (macOS or Windows).
  • Run the installer and accept all defaults.
  • Close and reopen the VS Code terminal, then run node --version again to confirm it prints a version number at or above 20.19.

Still not seeing a version number?

Make sure you closed and reopened the terminal after installing. The terminal needs a fresh session to pick up new programs. If it still does not work, try restarting VS Code entirely.

Help me troubleshoot Node.js not being recognized in my VS Code terminal.

Scaffold a new Vite project

Vite provides a scaffold command that generates a ready-to-run project folder with all the starter files you need. You will use the vanilla JavaScript template since this project does not use a framework.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Vite project by running this command:
npm create vite@latest trivia-quiz -- --template vanilla

What does this command do?

  • npm create vite@latest downloads and runs the Vite project generator.
  • trivia-quiz is the name of the folder it creates.
  • -- --template vanilla tells it to use plain JavaScript with no framework.

Seeing a prompt about installing a package?

npm may ask: "Need to install the following packages: create-vite@latest. Ok to proceed? (y)". Type y and press Enter to continue. This only appears the first time you run the command.

  • Move into the new project folder and install its dependencies by running these commands:
cd trivia-quiz
npm install

What just happened?

cd trivia-quiz moves your terminal into the project folder Vite created. npm install reads the package.json file and downloads all the packages the project needs into a node_modules folder.

  • Confirm the project files exist by running:
ls

You should see index.html, package.json, node_modules, and a src folder in the output.

Don't see these files?

Make sure you ran cd trivia-quiz before ls. If the folder does not exist, re-run the scaffold command from your Desktop and check for typos in trivia-quiz.

Help me figure out why my Vite scaffold did not create the project files.

Start the Vite dev server

Everything is installed. Time to start the dev server and see your app running in the browser.

  • Before you run the next command, what do you think you will see in the browser? Make a guess.
  • Start the development server by running:
npm run dev

What does this command do?

npm run dev starts a local web server on your machine. It watches your files and refreshes the browser automatically whenever you save a change. The server takes over this terminal session, so leave it running.

  • Look at your terminal output. You should see a line showing http://localhost:5173 (or a similar local URL).
  • Hold Cmd (macOS) or Ctrl (Windows) and click the URL in the terminal to open it in your browser.

You should see the Vite + Vanilla JS starter page with a heading, the Vite logo, and a counter button that increments when you click it.

Keep this terminal running

The dev server needs to stay running for the rest of the project. If you need to run other terminal commands later, open a second terminal in VS Code by pressing Ctrl+Shift+`.

Don't see the starter page?

If the browser shows "This site can't be reached", confirm the terminal still shows the server running (not an error). Try pasting http://localhost:5173 directly into your browser address bar.

If you see an error in the terminal instead of a URL, make sure you ran npm install before npm run dev.

Help me get the Vite dev server running and see the starter page.

Your development environment is ready. Next up, you will replace the starter template with your quiz interface and fetch live trivia questions from the internet.

Build the Quiz Interface and Fetch Live Questions

In Step 1, you got your development environment running with Vite serving a starter page in the browser.

Now it is time to turn that starter page into your actual quiz app. Before you can add game logic (click handling, scoring), you need the visual structure in place and real trivia data flowing in from the internet. By the end of this step, you will see a live trivia question appear on screen.

In this step, get ready to:
  • Build the quiz HTML with a start screen, question screen, and results screen.
  • Style the quiz with a dark theme, card layout, and answer buttons.
  • Fetch 10 live trivia questions from the Open Trivia Database API.
Delete template files and build the quiz HTML

The Vite starter template comes with files you do not need. You will remove them and replace the HTML with three screens: a start screen, a question screen, and a results screen.

  • Delete the two template files you do not need by running this command in your terminal:
rm src/counter.js src/javascript.svg
  • Confirm the files are gone by running:
ls src/

You should see only main.js and style.css remaining.

On Windows and rm doesn't work?

If you are using PowerShell, run Remove-Item src/counter.js, src/javascript.svg instead.

Help me delete files in my terminal.

  • Open index.html in the VS Code Explorer sidebar (it is in the root of your trivia-quiz folder).
  • Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows), delete it, then paste the following HTML structure:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Trivia Quiz</title>
  </head>
  <body>
    <div id="app">
      <div id="quiz-container">
        <div id="start-screen">
          <h1>Trivia Quiz</h1>
          <p>Test your knowledge with 10 random trivia questions!</p>
          <button id="start-btn">Start Quiz</button>
        </div>
      </div>
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

What does this HTML do?

  • The #app and #quiz-container divs create the card that holds the entire quiz.
  • The #start-screen has a heading, a description, and the Start Quiz button. This is what the player sees first.
  • The <script type="module"> tag connects Vite to your JavaScript entry point during development.
  • Save index.html and check your browser.

You should see the text "Trivia Quiz" and an unstyled "Start Quiz" button on a white page. The Vite starter content is gone.

Now add the question screen and results screen. These two sections start hidden and will appear when the player clicks Start Quiz.

  • In index.html, find the closing </div> tag for #start-screen. Place your cursor on the next line, then paste this code below it (still inside the #quiz-container div):
        <div id="question-screen" class="hidden">
          <div id="progress">
            <span id="question-number">Question 1 of 10</span>
            <span id="score">Score: 0</span>
          </div>
          <h2 id="question-text"></h2>
          <div id="answers"></div>
          <button id="next-btn" class="hidden">Next Question</button>
        </div>
        <div id="results-screen" class="hidden">
          <h1>Quiz Complete!</h1>
          <p id="final-score"></p>
          <p id="score-message"></p>
          <button id="restart-btn">Play Again</button>
        </div>

What do these screens do?

  • The #question-screen holds the progress bar, question text, answer buttons, and a Next button. The class="hidden" keeps it invisible until the game starts.
  • The #results-screen shows the final score and a Play Again button after all questions are answered.
  • Save index.html.
  • The browser still shows only the start screen since the new sections have class="hidden". To confirm they exist, right-click the page, select Inspect, and look in the Elements panel. You should see #question-screen and #results-screen inside #quiz-container.

Don't see the new elements in DevTools?

  • Make sure you pasted the code between the closing </div> of #start-screen and the closing </div> of #quiz-container. If they are outside the container, the layout breaks.
  • Check that the file is saved (look for the dot on the tab name in VS Code).

Help me fix my index.html structure.

✔️ Awesome, I've got everything!

Great. Make sure index.html is saved before moving on to styling.

ⓧ I'd like to double check the full code

Your index.html should contain both chunks you pasted above, in this order:

  • The full HTML shell with #start-screen (first code block above).
  • The #question-screen and #results-screen sections (second code block above), placed inside #quiz-container after #start-screen closes.

Scroll up and confirm each block is present and in order.

Style the quiz with a dark theme

Your HTML structure is in place but the page looks bare. You will replace the default Vite styles with a dark-themed card layout and styled answer buttons.

  • Open src/style.css in VS Code. Select all existing content with Cmd+A (macOS) or Ctrl+A (Windows), delete it, then paste this base layout styling:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  background-color: #1a1a2e;
  color: #eee;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

#app {
  width: 100%;
  max-width: 600px;
  padding: 2rem;
}

#quiz-container {
  background-color: #16213e;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

What does this CSS do?

  • The * reset removes default browser margins and padding so everything starts from a clean slate.
  • The body uses flexbox to vertically and horizontally center the quiz card on a dark navy background.
  • The #quiz-container creates a darker rounded card with a subtle shadow, giving the quiz a floating panel look.
  • Save src/style.css and check your browser.

You should see the page turn dark navy with your "Trivia Quiz" heading and button sitting inside a darker rounded card in the center.

  • Below the existing CSS in src/style.css, paste the heading and layout styles for the quiz structure:
h1 {
  font-size: 2rem;
  margin-bottom: 1rem;
  text-align: center;
}

h2 {
  font-size: 1.3rem;
  margin-bottom: 1.5rem;
  line-height: 1.4;
}

#progress {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1.5rem;
  font-size: 0.9rem;
  color: #aaa;
}

#answers {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

What does this CSS do?

  • The h1 and h2 rules center the title and set readable sizes for question text.
  • The #progress bar uses justify-content: space-between to push the question number and score to opposite sides.
  • The #answers container stacks answer buttons vertically with a gap between each one.
  • Continue by pasting the answer button styles below the existing CSS:
.answer-btn {
  padding: 1rem;
  border: 2px solid #0f3460;
  border-radius: 8px;
  background-color: #0f3460;
  color: #eee;
  font-size: 1rem;
  cursor: pointer;
  text-align: left;
  transition: background-color 0.2s, border-color 0.2s;
}

.answer-btn:hover {
  background-color: #1a4a7a;
  border-color: #1a4a7a;
}

.answer-btn.correct {
  background-color: #1b8a2d;
  border-color: #1b8a2d;
}

.answer-btn.wrong {
  background-color: #8a1b1b;
  border-color: #8a1b1b;
}

.answer-btn.disabled {
  pointer-events: none;
}

What do these button classes do?

  • The .answer-btn base style creates dark blue rounded buttons with a smooth hover transition.
  • The .correct, .wrong, and .disabled classes will be added by JavaScript later to give instant color feedback when a player picks an answer.
  • Save src/style.css and check your browser.

Your heading should now be centered with the description text below it. The Start Quiz button is still unstyled. You will fix that next.

  • Paste the action button shared styles below the existing CSS:
button#start-btn,
button#next-btn,
button#restart-btn {
  display: block;
  width: 100%;
  padding: 1rem;
  margin-top: 1.5rem;
  border: none;
  border-radius: 8px;
  background-color: #e94560;
  color: #fff;
  font-size: 1.1rem;
  font-weight: bold;
  cursor: pointer;
  transition: background-color 0.2s;
}

button#start-btn:hover,
button#next-btn:hover,
button#restart-btn:hover {
  background-color: #d63851;
}

What does this CSS do?

The Start, Next, and Restart buttons all share a bold pink style that makes them stand out as the primary action on each screen. The hover state darkens the pink slightly for feedback.

  • Paste these final styles at the bottom of src/style.css:
#start-screen p,
#results-screen p {
  text-align: center;
  margin-bottom: 0.5rem;
  font-size: 1.1rem;
  color: #ccc;
}

#final-score {
  font-size: 2rem;
  font-weight: bold;
  color: #e94560;
}

#score-message {
  font-size: 1.1rem;
}

.hidden {
  display: none;
}

What does this CSS do?

  • The paragraph styles center the description text and results text with a light gray color.
  • The .hidden class sets display: none. JavaScript will toggle this class to show and hide screens throughout the game.
  • Save src/style.css and check your browser.

Your Start Quiz button should now be a bold pink rectangle stretching the full width of the card. The description text is centered and light gray. Hover over the button to see it darken slightly.

Button still looks unstyled?

  • Make sure you pasted each CSS block below the previous one. If blocks overlap or are pasted inside another rule, styles break.
  • Check for any missing closing } braces. Each selector must have matching open and close braces.

Help me fix my quiz CSS.

✔️ Awesome, I've got everything!

Great. Your styling is complete. Make sure src/style.css is saved before moving on to the JavaScript.

ⓧ I'd like to double check the full code

Your src/style.css should contain all five blocks you pasted above, in this order:

  • Base layout (reset, body, #app, #quiz-container).
  • Headings, #progress, and #answers layout.
  • .answer-btn base style plus hover, .correct, .wrong, and .disabled states.
  • Action button shared styles (start, next, restart) and their hover state.
  • Paragraph text styles, #final-score, #score-message, and the .hidden utility.

Scroll up and confirm each block is present and in order with no overlapping braces.

Fetch live trivia questions with JavaScript

The structure and style are ready. Now you will write the JavaScript that connects to the Open Trivia Database API, fetches 10 questions, and displays the first one when the player clicks Start Quiz.

  • Open src/main.js in VS Code. Select all existing content, delete it, then paste this code that sets up the state variables and grabs references to every DOM element you will need:
import './style.css';

// Game state
let questions = [];
let currentQuestionIndex = 0;
let score = 0;

// DOM element references
const startScreen = document.getElementById('start-screen');
const questionScreen = document.getElementById('question-screen');
const resultsScreen = document.getElementById('results-screen');
const startBtn = document.getElementById('start-btn');
const nextBtn = document.getElementById('next-btn');
const restartBtn = document.getElementById('restart-btn');
const questionText = document.getElementById('question-text');
const answersContainer = document.getElementById('answers');
const questionNumber = document.getElementById('question-number');
const scoreDisplay = document.getElementById('score');
const finalScore = document.getElementById('final-score');
const scoreMessage = document.getElementById('score-message');

What does this code do?

  • The import './style.css' line tells Vite to bundle your styles with the app.
  • The three let variables track the game state: the fetched questions, which question is currently showing, and the player's score.
  • Each document.getElementById call grabs a reference to an HTML element so you can show, hide, or update it later without searching the DOM repeatedly.
  • Below that code in the same file, paste the function that fetches questions from the API and the click handler that starts the quiz:
// Fetch 10 trivia questions from the Open Trivia Database
async function fetchQuestions() {
  const response = await fetch(
    'https://opentdb.com/api.php?amount=10&type=multiple'
  );
  const data = await response.json();
  return data.results;
}

// Start the quiz when the button is clicked
startBtn.addEventListener('click', async () => {
  startBtn.textContent = 'Loading...';
  startBtn.disabled = true;
  questions = await fetchQuestions();
  currentQuestionIndex = 0;
  score = 0;
  startScreen.classList.add('hidden');
  questionScreen.classList.remove('hidden');
  startBtn.textContent = 'Start Quiz';
  startBtn.disabled = false;
  const question = questions[0];
  questionText.textContent = question.question;
  answersContainer.innerHTML = '';
  const answers = [...question.incorrect_answers, question.correct_answer];
  answers.forEach((answer) => {
    const button = document.createElement('button');
    button.classList.add('answer-btn');
    button.textContent = answer;
    answersContainer.appendChild(button);
  });
});

What does this code do?

  • The fetchQuestions function uses fetch and async/await to call the Open Trivia Database API and return 10 multiple-choice questions.
  • The addEventListener on startBtn waits for a click, fetches the questions, swaps the start screen for the question screen, then builds four answer buttons from the first question's data.
  • The button text changes to "Loading..." while the API request is in flight so the player knows something is happening.
  • Save src/main.js.

Before you click Start Quiz, what do you think will happen? Will the question text look perfectly clean, or might there be something odd about it?

  • Click the Start Quiz button in your browser.

You should see a real trivia question appear with four dark blue answer buttons below it. Look closely at the question text. You will likely notice garbled characters like &quot; instead of proper quotes, or &#039; instead of apostrophes. That is expected.

Why does the text look garbled?

The Open Trivia Database returns text with HTML entities (like &quot; for quotes). When you assign this to .textContent, the browser displays the raw entities instead of converting them. This is a known quirk of the API that you will solve with a decode helper in the next step.

Nothing happens when you click Start Quiz?

  • Check the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) for red error messages. A common cause is a typo in an element ID that does not match the HTML.
  • Make sure the import './style.css' line is the very first line in main.js. Without it, Vite cannot connect the module.
  • Confirm that your Vite dev server is still running in the terminal. If it stopped, run npm run dev again.

Help me debug why Start Quiz does nothing.

✔️ Awesome, I've got everything!

Great. Make sure src/main.js is saved before moving to the next step.

ⓧ I'd like to double check the full code

Your src/main.js should contain both chunks you pasted above, in this order:

  • The import, state variables, and all DOM element references (first code block above).
  • The fetchQuestions function and the startBtn click event listener (second code block above).

Scroll up and confirm each block is present and in order.

You have a live trivia quiz pulling real questions from the internet. The garbled text is a known API quirk that you will fix next. In the next step, you will decode that text and make the answer buttons respond to clicks with instant correct/wrong feedback.

Fix Garbled Text and Make Answers Interactive

In the last step, you fetched live trivia questions and saw them appear on screen. But something looked off. The text was full of garbled characters like &quot; and &#039; instead of proper quotation marks and apostrophes.

That happened because the Open Trivia Database returns HTML-encoded text by default. In this step, you'll decode those entities into readable text, randomize the answer order, and make the buttons respond to clicks with instant green or red feedback.

In this step, get ready to:
  • Fix garbled HTML entities so questions display as readable text.
  • Shuffle answer positions so the correct answer moves around randomly.
  • Add click feedback that highlights answers green (correct) or red (wrong).
Add helper functions for decoding and shuffling

The API encodes special characters as HTML entities. You need a function to convert them back to normal text, and another function to randomize answer order so the correct answer isn't always in the last position.

  • In src/main.js, find the line that says async function fetchQuestions().
  • Add the following two helper functions directly above it:
function decodeHTML(html) {
  const textarea = document.createElement('textarea');
  textarea.innerHTML = html;
  return textarea.value;
}

What does decodeHTML do?

This trick uses a hidden textarea element as a decoder. When you set innerHTML to a string containing HTML entities, the browser automatically converts them to real characters. Reading .value back gives you the decoded text.

  • Now add this shuffleArray function directly below fetchQuestions:
function shuffleArray(array) {
  const shuffled = [...array];
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }
  return shuffled;
}

What does shuffleArray do?

This uses the Fisher-Yates algorithm to randomize an array. It loops backwards through the array, swapping each element with a random earlier element. The spread operator [...array] creates a copy so the original array stays untouched.

Build the question display function

Right now the startBtn listener has all the display logic jammed inside it. You'll extract that into a dedicated showQuestion function that uses both helpers to display decoded, shuffled questions.

  • Add this showQuestion function below shuffleArray:
function showQuestion() {
  const question = questions[currentQuestionIndex];
  questionNumber.textContent = `Question ${currentQuestionIndex + 1} of ${questions.length}`;
  scoreDisplay.textContent = `Score: ${score}`;
  questionText.textContent = decodeHTML(question.question);

  const answers = shuffleArray([
    ...question.incorrect_answers,
    question.correct_answer,
  ]);

  answersContainer.innerHTML = '';
  answers.forEach((answer) => {
    const button = document.createElement('button');
    button.classList.add('answer-btn');
    button.textContent = decodeHTML(answer);
    button.addEventListener('click', () =>
      selectAnswer(button, answer, question.correct_answer)
    );
    answersContainer.appendChild(button);
  });

  nextBtn.classList.add('hidden');
}

What does showQuestion do?

  • Updates the progress bar with the current question number and score.
  • Uses decodeHTML on both the question text and each answer so entities display as real characters.
  • Combines the incorrect and correct answers into one array, then shuffles them with shuffleArray.
  • Creates a button for each answer and attaches a click listener that calls selectAnswer (you'll add this function next).
Make answer buttons respond to clicks

When a learner clicks an answer, the app needs to lock all the buttons, show whether the pick was right or wrong, reveal the correct answer, and display a Next button.

  • Add this selectAnswer function directly below showQuestion:
function selectAnswer(button, selected, correct) {
  const buttons = answersContainer.querySelectorAll('.answer-btn');
  buttons.forEach((btn) => btn.classList.add('disabled'));

  if (selected === correct) {
    button.classList.add('correct');
    score++;
    scoreDisplay.textContent = `Score: ${score}`;
  } else {
    button.classList.add('wrong');
    buttons.forEach((btn) => {
      if (btn.textContent === decodeHTML(correct)) {
        btn.classList.add('correct');
      }
    });
  }

  nextBtn.classList.remove('hidden');
}

What does selectAnswer do?

  • Adds the .disabled class to every button so no further clicks register.
  • If the selected answer matches the correct one, adds the .correct class (turns green) and increments the score.
  • If wrong, adds .wrong to the picked button (turns red) and highlights the actual correct answer in green.
  • Shows the nextBtn so the learner can advance to the next question.

Now you need to update the Start button's click listener to use your new showQuestion function instead of the inline display code.

  • Scroll down to the startBtn.addEventListener block at the bottom of the file.
  • Find these lines at the end of the listener (everything after the line that re-enables the button):
  const question = questions[0];
  questionText.textContent = question.question;
  answersContainer.innerHTML = '';
  const answers = [...question.incorrect_answers, question.correct_answer];
  answers.forEach((answer) => {
    const button = document.createElement('button');
    button.classList.add('answer-btn');
    button.textContent = answer;
    answersContainer.appendChild(button);
  });
  • Delete all of those lines and replace them with a single call to showQuestion():
  showQuestion();

Before you test this, what do you think you'll see when you click Start Quiz now? Will the garbled characters be gone?

  • Save src/main.js.
  • Switch back to your browser at http://localhost:5173.
  • Click Start Quiz.

You should see a properly formatted question with real quotation marks and apostrophes. The four answer buttons appear in a randomized order. Click one of the answers. It turns green if you got it right, or red if you got it wrong with the correct answer highlighted in green.

Still seeing garbled text or buttons not responding?

  • Check that decodeHTML is defined as a standalone function above fetchQuestions. If it's nested inside another function, it won't be accessible.
  • Make sure you deleted ALL of the old inline display code from the startBtn listener and replaced it with showQuestion(); only.
  • If clicking an answer does nothing, confirm that selectAnswer is defined at the top level of the file (not inside another function).

Help me debug my quiz answer buttons

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.js and confirmed that clicking an answer shows green or red feedback.

ⓧ I'd like to double check the full code

Here is the complete src/main.js at this point. Scroll through and confirm each section is present in order:

  • The import './style.css' line, state variables, and DOM element selections at the top.
  • The decodeHTML function.
  • The fetchQuestions function.
  • The shuffleArray function.
  • The showQuestion function.
  • The selectAnswer function.
  • The startBtn click listener ending with showQuestion();.

Your quiz now shows clean, readable questions and gives instant feedback when you pick an answer. Next up, you'll wire up the full game loop so you can play through all 10 questions and see your final score.

Track Score and Show Results

Your quiz now handles individual questions with instant correct/wrong feedback. But the Next Question button does nothing, and after answering you are stuck on the same question forever.

In this step, you will wire up the full game loop. The learner clicks through all 10 questions, sees a final results screen with a performance message, and can start over with fresh questions any time.

In this step, get ready to:
  • Show a results screen with your final score and a performance message.
  • Wire up the Next Question button to advance through all 10 questions.
  • Add a Play Again button that fetches a fresh set of questions.
Add the results screen logic

Score tracking is already working from the previous step. Every time the learner picks the correct answer, selectAnswer increments score and updates the display. Now you need a function that shows the final results when the quiz ends.

  • In src/main.js, add the following showResults function below the selectAnswer function:
function showResults() {
  // Hide the question screen, show the results screen
  questionScreen.classList.add('hidden');
  resultsScreen.classList.remove('hidden');
  // Display the final score as "X / 10"
  finalScore.textContent = `${score} / ${questions.length}`;

  // Show a message based on performance
  if (score === questions.length) {
    scoreMessage.textContent = 'Perfect score! You are a trivia master!';
  } else if (score >= questions.length * 0.7) {
    scoreMessage.textContent = 'Great job! You really know your stuff!';
  } else if (score >= questions.length * 0.4) {
    scoreMessage.textContent = 'Not bad! Keep learning and try again!';
  } else {
    scoreMessage.textContent = 'Better luck next time! Practice makes perfect!';
  }
}

What does this code do?

This function runs when the player finishes all 10 questions. It swaps the question screen for the results screen and displays the score.

The if/else chain picks a message based on performance thresholds: perfect (10/10), great (70%+), okay (40%+), or needs practice (below 40%).

Wire up the Next Question button

The showResults function exists now but nothing calls it yet. The Next Question button needs a click listener that either advances to the next question or, when all questions are done, triggers the results.

  • Below the existing startBtn.addEventListener block, add a click listener for the Next button:
nextBtn.addEventListener('click', () => {
  currentQuestionIndex++;
  // If there are more questions, show the next one
  if (currentQuestionIndex < questions.length) {
    showQuestion();
  } else {
    // All questions answered, show results
    showResults();
  }
});

What does this code do?

  • currentQuestionIndex++ moves to the next question in the array.
  • The if check decides whether to show another question or end the quiz. When the index reaches the total number of questions, the game is over.
  • Save src/main.js.

Before you test, think about what will happen when you answer the last question and click Next. Do you think the results screen will appear?

  • Switch to your browser at http://localhost:5173.
  • Click Start Quiz and play through all 10 questions (click any answer, then click Next Question each time).

After the 10th question, you should see the results screen with "Quiz Complete!" at the top, your score displayed as something like "7 / 10", and a performance message below it.

Not seeing the results screen?

  • Make sure the nextBtn.addEventListener block is placed after (below) the startBtn.addEventListener block, not inside it.
  • Check that the showResults function is defined above the listener that calls it. JavaScript needs to see the function before it can be used.
  • If the quiz stays stuck on one question, check that you are clicking the Next Question button that appears after answering.

Why isn't my results screen showing after the last question?

Add the Play Again button

The results screen has a Play Again button, but clicking it does nothing. You need a listener that fetches a fresh set of questions and resets the game state so the player can try again.

  • Below the nextBtn listener you just added, add a click listener for the Play Again button:
restartBtn.addEventListener('click', async () => {
  // Show loading state while fetching new questions
  restartBtn.textContent = 'Loading...';
  restartBtn.disabled = true;
  // Fetch a fresh set of 10 questions
  questions = await fetchQuestions();
  currentQuestionIndex = 0;
  score = 0;
  // Switch from results back to question screen
  resultsScreen.classList.add('hidden');
  questionScreen.classList.remove('hidden');
  restartBtn.textContent = 'Play Again';
  restartBtn.disabled = false;
  showQuestion();
});

What does this code do?

  • The button text changes to "Loading..." so the player knows new questions are being fetched from the API.
  • fetchQuestions() grabs 10 brand new random questions from the Open Trivia Database.
  • Resetting currentQuestionIndex and score to 0 ensures the new round starts fresh with no carryover from the last game.
  • Save src/main.js.
  • In your browser, play through all 10 questions again until you reach the results screen.
  • Click Play Again.

You should see the button briefly say "Loading..." then a brand new question appears. The score resets to 0 and the question counter starts at "Question 1 of 10" again.

Play Again button not working?

  • Make sure the listener uses async before the arrow function. Without it, await fetchQuestions() will throw a syntax error.
  • If the button stays stuck on "Loading..." the API might be rate-limited. Wait 5 seconds and try again.
  • Check your browser console (F12) for red error messages. A typo in restartBtn or fetchQuestions will show up there.

Why does my Play Again button not fetch new questions?

✔️ Awesome, I've got everything!

Double check you have saved src/main.js and that your quiz plays through all 10 questions, shows results, and restarts with fresh questions.

ⓧ I'd like to double check the full code

Your src/main.js should contain these sections in order. Scroll up through your file and confirm each one is present:

  • import './style.css'; at the top.
  • Three state variables: questions, currentQuestionIndex, score.
  • All DOM element constants (startScreen through scoreMessage).
  • decodeHTML function.
  • fetchQuestions async function.
  • shuffleArray function.
  • showQuestion function.
  • selectAnswer function.
  • showResults function (added this step).
  • startBtn click listener.
  • nextBtn click listener (added this step).
  • restartBtn click listener (added this step).

Your trivia quiz is fully playable. You have a complete game loop where questions load, answers give feedback, scores accumulate, results display, and players can restart. Next up, you will take on the Secret Mission and add difficulty and category options to customize the quiz.

Secret mission

Add a Difficulty and Category Selector

Right now your quiz serves random questions at random difficulty. What if you could choose how hard the questions are, and pick a specific topic like Science or History? In this mission, you'll build a settings form that customizes your quiz before it starts.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using a free API, so there are no ongoing costs. Decide whether to keep your project folder as-is, stop the dev server to free up your terminal, or delete the project entirely.

Resources you used:

  • The trivia-quiz project folder on your Desktop (contains index.html, src/main.js, src/style.css, and installed npm packages).
  • Vite dev server (running process in your terminal).

Keep everything running

No action needed. Choose this if you are still actively building or want to come back and extend the quiz.

  • Leave the trivia-quiz folder on your Desktop.
  • Run npm run dev from inside the folder any time to start the app again.
  • Node.js remains installed on your machine for future JavaScript projects.

Pause - I'll come back to this later

Stop the running process to free up your terminal. Your files stay on disk, ready to pick up later.

  • Switch to the terminal where Vite is running and press Ctrl+C to stop the dev server.
  • Your trivia-quiz folder remains on your Desktop. Run npm run dev from inside it whenever you want to resume.

Delete - I don't want to use this again

Remove the project folder entirely. There are no cloud resources or accounts to clean up.

  • Switch to the terminal where Vite is running and press Ctrl+C to stop the dev server.
  • Delete the project folder by running the command for your operating system.

macOS

cd ~/Desktop
rm -rf trivia-quiz

Windows

cd ~\Desktop
Remove-Item -Recurse -Force trivia-quiz
  • Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see trivia-quiz listed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully interactive trivia quiz game that pulls live questions from the internet and runs entirely in your browser.

You've learned how to:

  • Built an interactive UI by selecting and manipulating DOM elements with JavaScript.
  • Fetched live data from a REST API using fetch and async/await.
  • Handled user events (clicks) and managed application state with JavaScript variables to drive an entire game loop.
  • Secret Mission: Built a dynamic settings form that customizes API requests with query parameters.

Ready to quiz yourself?