Build a Quiz App with React

Build a multi-screen React quiz app with state, props, and components.

Introduction

30 Second Summary

Quizzes are everywhere. From language-learning apps to onboarding flows at work, interactive question-and-answer experiences are one of the most common patterns in web apps.

In this project, you will build a multi-screen quiz app with React that presents questions, tracks your score, and shows results.

What You'll Build

A fully interactive quiz app built with React and Vite that displays questions one at a time, gives immediate feedback on answers, and presents a final results screen.

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

  • A React project scaffolded with Vite, running locally with hot-reload.
  • Five React components that communicate using props and state to drive a start screen, question flow, and results screen.
  • Hands-on experience using Claude Code's Plan Mode to architect a React app before building it.
  • Secret Mission: add a countdown timer that auto-advances when time runs out.

Are there any prerequisites?

You should be comfortable with basic JavaScript (ES6+), HTML, and CSS. No prior React experience is needed.

Set Up Your React Project with Vite

Before you can build the quiz app, you need a React project running on your machine. React is a JavaScript library for building user interfaces out of reusable components.

In this step, you'll use Vite to scaffold a new React project, start a development server, and set up Claude Code with a project memory file called CLAUDE.md.

In this step, get ready to:
  • Scaffold a new React project with Vite's React template.
  • Run the development server and verify the default page loads.
  • Open the project in Claude Code and create a CLAUDE.md file.
Scaffold a new React project with Vite

Vite is a modern build tool that scaffolds projects instantly and serves them with hot module replacement. It replaced Create React App, which was deprecated in 2025.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a new React project by running this command:
npm create vite@latest react-quiz -- --template react

What does this command do?

  • npm create vite@latest runs Vite's project scaffolding tool at the latest version.
  • react-quiz is the name of the folder it creates.
  • -- --template react tells Vite to use the React starter template, which includes JSX support and React dependencies.
  • Confirm the project folder was created by running:
ls

You should see react-quiz in the output.

Don't see the react-quiz folder?

  • Make sure you ran the command from your Desktop. Run pwd to check your current directory.
  • Vite requires Node.js version 20.19+ or 22.12+. Run node --version to check yours.

Still stuck? Help me troubleshoot the Vite scaffold command failing.

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

What does npm install do?

Your project's package.json lists the libraries it needs (React, Vite, and their plugins). npm install downloads all of them into a node_modules folder so your project can use them.

You should see output mentioning packages being added and a confirmation like "added X packages."

npm install failing?

  • Check that you're inside the react-quiz folder. Run pwd and confirm the path ends with react-quiz.
  • If you see permission errors, try running the command again without sudo. Using sudo with npm can cause ownership issues.

Need more help? Help me fix npm install errors in my React project.

Run the development server
  • Start the Vite development server by running:
npm run dev
  • Open http://localhost:5173 in your browser.

You should see the default Vite + React page with the Vite and React logos, a counter button, and links to documentation.

What is a dev server?

A development server runs your app locally and watches for file changes. When you edit and save a file, Vite automatically refreshes the browser with your changes. No manual page refresh needed.

Don't see the page?

  • Make sure you typed the full URL including the port: http://localhost:5173.
  • Check your terminal for error messages. The server should show a "Local:" URL when it starts successfully.
  • If port 5173 is already in use, Vite will pick a different port and display it in the terminal output.

Still stuck? Help me get the Vite dev server running.

Open the project in Claude Code and create a CLAUDE.md file

Claude Code works best when it understands your project's context. A CLAUDE.md file at the project root acts as persistent memory, telling Claude Code what the project is about and how to write code for it.

  • Open a new terminal tab by pressing Ctrl+Shift+` (macOS) or Ctrl+Shift+` (Windows).
  • Navigate to your project and launch Claude Code by running these commands:
cd ~/Desktop/react-quiz
claude

Claude Code opens as an interactive session in your terminal with an input area at the bottom where you type messages.

What is CLAUDE.md?

CLAUDE.md is a project memory file that Claude Code reads automatically when it opens a project. It tells Claude your tech stack, preferred patterns, and file structure so it generates code that fits your project from the start.

  • In Claude Code, create the CLAUDE.md file by typing this message and pressing Enter:
Create a CLAUDE.md file in the project root with the following content:

# React Quiz App

## Purpose
An interactive quiz app that presents multiple-choice questions, tracks scores, and shows results.

## Tech Stack
- React (with Vite as the build tool)
- Plain CSS for styling
- No external libraries beyond React

## Coding Conventions
- Functional components only (no class components)
- useState for state management
- Props for passing data from parent to child components
- One component per file in src/components/

## File Structure
- src/main.jsx - App entry point
- src/App.jsx - Root component managing app state
- src/App.css - Global styles
- src/components/ - Individual React components
- src/data/ - Quiz question data

What does each section do?

  • Purpose tells Claude what the app does, so it generates relevant code.
  • Tech Stack prevents Claude from pulling in unnecessary libraries or TypeScript.
  • Coding Conventions ensures Claude uses functional components and hooks instead of class components.
  • File Structure helps Claude place new files in the right directories.
  • After Claude Code creates the file, verify it exists by typing:
List the contents of CLAUDE.md

Claude Code should display the contents of your CLAUDE.md file, showing all four sections (Purpose, Tech Stack, Coding Conventions, File Structure).

Claude Code didn't create the file?

  • If Claude Code asks for permission to write files, type yes or press Enter to allow it.
  • Make sure you launched claude from inside the react-quiz folder. Claude Code creates files relative to where you started it.

Need help? Help me get Claude Code to create my CLAUDE.md file.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file and your dev server is still running in the other terminal tab.

ⓧ I'd like to double check the full code

Your CLAUDE.md file at the project root should contain something like this:

# React Quiz App

## Purpose
An interactive quiz app that presents multiple-choice questions, tracks scores, and shows results.

## Tech Stack
- React (with Vite as the build tool)
- Plain CSS for styling
- No external libraries beyond React

## Coding Conventions
- Functional components only (no class components)
- useState for state management
- Props for passing data from parent to child components
- One component per file in src/components/

## File Structure
- src/main.jsx - App entry point
- src/App.jsx - Root component managing app state
- src/App.css - Global styles
- src/components/ - Individual React components
- src/data/ - Quiz question data

Your React project is scaffolded, the dev server is running, and Claude Code knows your project inside and out. Next up, you'll use Claude Code's Plan Mode to architect the full quiz app before writing any code.

Plan the Quiz App Architecture in Plan Mode

Your React project is scaffolded, the dev server is running, and Claude Code knows your project through CLAUDE.md. Now it's time to plan what you're actually building.

Professional developers don't jump straight into code. They map out the structure first. Claude Code has a Plan Mode specifically for this. You describe what you want, Claude proposes an architecture, and you approve it before any code gets written.

In this step, get ready to:
  • Describe the quiz app requirements to Claude in Plan Mode.
  • Review and approve the proposed component architecture.
  • Have Claude create the question data file.
Enter Plan Mode and describe the quiz app requirements

Plan Mode tells Claude Code to read your project and propose a plan without making any changes. You get to review and approve everything first.

  • Switch to Plan Mode by pressing Shift+Tab in Claude Code.
  • You may need to press Shift+Tab more than once to cycle through permission modes until you see "plan" indicated in the interface.
  • Once in Plan Mode, describe the quiz app by typing a prompt like this:
I want to build a quiz app with these features:
- A start screen with a title, description, and "Start Quiz" button
- Questions shown one at a time with 4 multiple-choice options
- Score tracking throughout the quiz
- Immediate feedback when an answer is selected (green for correct, red for incorrect)
- A results screen showing the final score and a "Play Again" button

Please propose a component architecture and data structure for the questions.

What is Plan Mode?

In Plan Mode, Claude Code reads your files and proposes a plan but makes no edits until you approve. This gives you a chance to review the architecture before any code touches your project.

The prompt above isn't an exact script you must follow. Feel free to describe the app in your own words. The key requirements are: start screen, questions one at a time, 4 options, score tracking, answer feedback, and results screen.

After you submit, Claude will think through the architecture and present a plan. Don't approve it yet.

Review Claude's proposed component architecture

Claude should propose a component structure that breaks the app into logical pieces. Each component handles one responsibility.

  • Look for these components in Claude's proposed plan:
  • Check that Claude proposes a data structure for questions with fields like id, question, options, and answer.
  • If the plan looks good, approve it. If something is missing (like immediate answer feedback), ask Claude to include it before approving.

Why plan before coding?

Planning forces you to think about how components communicate before writing any code. It's much easier to move boxes on a whiteboard than to refactor a tangled codebase.

Claude might propose a separate Question component. That's fine at the planning stage. During implementation, you might fold it into QuizScreen for simplicity.

Claude didn't propose separate components?

If Claude suggests putting everything in one file, refine your prompt. Ask it to break the app into separate components: one for the start screen, one for the quiz flow, and one for results. Mention that each component should live in its own file under src/components/.

help me get Claude to propose separate React components for my quiz app

Create the question data file

With the plan approved, it's time to generate the first piece of code. Separating quiz questions from UI logic keeps the app clean and makes it easy to swap in different question sets later.

  • Press Shift+Tab to cycle back to the default permission mode (so Claude can actually write files).
  • Ask Claude to create the question data file by typing a prompt like:
Create src/data/questions.js with 5 multiple-choice questions about React fundamentals. Each question should have an id, question text, 4 options array, and an answer field matching the correct option exactly.
  • Allow Claude to create the file when it asks for permission.

Why separate data from UI?

Putting questions in their own file means you can add, remove, or change questions without touching any component code. This pattern of separating data from presentation is a core principle in React development.

  • Verify the file was created by checking that src/data/questions.js appears in your project's file tree.
  • Open src/data/questions.js and confirm it has 5 questions, each with id, question, options (array of 4 strings), and answer fields.
  • Make sure the file exports the questions array as the default export.

Your file should look similar to this:

const questions = [
  {
    id: 1,
    question: "What hook is used to add state to a React component?",
    options: ["useEffect", "useState", "useContext", "useReducer"],
    answer: "useState"
  },
  {
    id: 2,
    question: "What syntax does React use to describe UI?",
    options: ["HTML", "XML", "JSX", "JSON"],
    answer: "JSX"
  },
  {
    id: 3,
    question: "How do you pass data from a parent to a child component?",
    options: ["State", "Props", "Context", "Refs"],
    answer: "Props"
  },
  {
    id: 4,
    question: "What does the virtual DOM do?",
    options: [
      "Replaces the real DOM entirely",
      "Efficiently updates only changed parts of the real DOM",
      "Stores data in the browser",
      "Handles network requests"
    ],
    answer: "Efficiently updates only changed parts of the real DOM"
  },
  {
    id: 5,
    question: "Which command creates a new React project with Vite?",
    options: [
      "npx create-react-app my-app",
      "npm create vite@latest my-app -- --template react",
      "npm init react-app my-app",
      "npx vite create my-app"
    ],
    answer: "npm create vite@latest my-app -- --template react"
  }
];

export default questions;

What does this data structure do?

Each question object has a unique id for React's key prop, the question text to display, an options array with exactly 4 choices, and an answer string that matches one of the options verbatim.

The answer field matching an option exactly is important. Later, the quiz logic will compare the user's selection against this string to determine if they got it right.

  • If Claude generated different questions than the ones shown above, that's fine. The important thing is the structure matches: id, question, options (4 items), answer (matches one option exactly), and export default questions at the bottom.
  • If you want to match this project exactly, replace Claude's generated content with the code shown above and save the file.

✔️ Awesome, I've got everything!

Great. Make sure you've saved src/data/questions.js.

ⓧ I'd like to double check the full code

Here's the complete src/data/questions.js file:

const questions = [
  {
    id: 1,
    question: "What hook is used to add state to a React component?",
    options: ["useEffect", "useState", "useContext", "useReducer"],
    answer: "useState"
  },
  {
    id: 2,
    question: "What syntax does React use to describe UI?",
    options: ["HTML", "XML", "JSX", "JSON"],
    answer: "JSX"
  },
  {
    id: 3,
    question: "How do you pass data from a parent to a child component?",
    options: ["State", "Props", "Context", "Refs"],
    answer: "Props"
  },
  {
    id: 4,
    question: "What does the virtual DOM do?",
    options: [
      "Replaces the real DOM entirely",
      "Efficiently updates only changed parts of the real DOM",
      "Stores data in the browser",
      "Handles network requests"
    ],
    answer: "Efficiently updates only changed parts of the real DOM"
  },
  {
    id: 5,
    question: "Which command creates a new React project with Vite?",
    options: [
      "npx create-react-app my-app",
      "npm create vite@latest my-app -- --template react",
      "npm init react-app my-app",
      "npx vite create my-app"
    ],
    answer: "npm create vite@latest my-app -- --template react"
  }
];

export default questions;

File not appearing in your project?

Make sure you cycled out of Plan Mode before asking Claude to create the file. In Plan Mode, Claude proposes changes but cannot write them. Press Shift+Tab to cycle back to the default mode, then ask again.

Also check that the file is at src/data/questions.js (inside a data folder inside src). If Claude put it somewhere else, ask it to move the file to the correct location.

help me troubleshoot why Claude Code didn't create my questions.js file

You now have a solid architectural plan and your first data file. Next up, you'll build the StartScreen component and wire up the App shell to manage which screen the user sees.

Build the StartScreen and App Shell

You've planned the app's architecture and created the question data file. Now it's time to build the first thing your users actually see.

In this step, you'll create the StartScreen component and set up the App shell that controls which screen is visible. The App shell uses React state to track where the user is in the quiz flow, then conditional rendering to show the right screen at the right time.

In this step, get ready to:
  • Build a StartScreen component with a title, description, and Start Quiz button.
  • Refactor App.jsx to manage which screen displays using state.
  • Test that clicking Start Quiz transitions the view.
Have Claude build the StartScreen component

The StartScreen is the first thing users see when the app loads. It's a simple component that displays a title, a brief description, and a button to kick off the quiz.

  • In Claude Code, ask Claude to create a StartScreen component.
  • Tell Claude the component should live at src/components/StartScreen.jsx, display a title ("React Quiz"), a description, a question count, and a "Start Quiz" button that calls an onStart prop when clicked.
  • After Claude generates the file, verify it looks like this:
function StartScreen({ onStart }) {
  return (
    <div className="start-screen">
      <h1>React Quiz</h1>
      <p>Test your knowledge of React fundamentals!</p>
      <p className="question-count">5 questions</p>
      <button className="btn btn-start" onClick={onStart}>
        Start Quiz
      </button>
    </div>
  );
}

export default StartScreen;

What does this code do?

This is your first custom React component. It's a function that returns JSX describing what should appear on screen.

  • { onStart } is destructuring the props object. The parent component will pass a function as the onStart prop.
  • onClick={onStart} wires the button to that function. When the user clicks "Start Quiz", the parent decides what happens next.
  • export default StartScreen makes the component importable from other files.

You won't see this component in the browser yet. The next chunk connects it to the App shell.

Refactor App.jsx to manage the app's stage

Right now, App.jsx still has the default Vite boilerplate (spinning logos and a counter). You need to replace it with a shell that tracks which screen the user is on.

  • Ask Claude to replace the contents of src/App.jsx with a component that manages a stage state (values: 'start', 'quiz', 'result') and conditionally renders StartScreen when stage is 'start'.
  • Tell Claude to include placeholder text for the quiz and result stages, plus handler functions for starting, finishing, and restarting.
  • Verify the generated code matches this:
import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import questions from './data/questions.js';

function App() {
  const [stage, setStage] = useState('start');
  const [finalScore, setFinalScore] = useState(0);

  const handleStart = () => setStage('quiz');

  const handleFinish = (score) => {
    setFinalScore(score);
    setStage('result');
  };

  const handleRestart = () => {
    setFinalScore(0);
    setStage('start');
  };

  return (
    <div className="app">
      {stage === 'start' && <StartScreen onStart={handleStart} />}
      {stage === 'quiz' && <div>Quiz coming soon...</div>}
      {stage === 'result' && <div>Results coming soon...</div>}
    </div>
  );
}

export default App;

What does this code do?

  • useState('start') creates a stage variable that tracks which screen is active. The useState hook gives you a value and a setter function to update it.
  • handleStart, handleFinish, and handleRestart are handler functions that change the stage. They get passed as props to child components.
  • The {stage === 'start' && <StartScreen />} pattern is conditional rendering. React only renders the component when the condition is true.

Why are there placeholder divs?

The Quiz coming soon... and Results coming soon... placeholders let you test the stage transitions now, before those components exist. You'll replace them with real components in the next steps.

Claude Code likely also updated src/main.jsx to import the stylesheet correctly. Check that it imports ./App.css instead of the default ./index.css.

  • In src/main.jsx, find this line:
import './index.css'
  • If it still says ./index.css, ask Claude to change it to ./App.css so it looks like this:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './App.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Now you need the CSS that makes the app look polished instead of unstyled HTML.

  • Ask Claude to create src/App.css with styles for the app container, start screen, and buttons.
  • Verify it matches this:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app {
  background: white;
  border-radius: 16px;
  padding: 40px;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.start-screen,
.result-screen {
  text-align: center;
}

.start-screen h1,
.result-screen h1 {
  font-size: 2rem;
  color: #333;
  margin-bottom: 12px;
}

.start-screen p {
  color: #666;
  margin-bottom: 8px;
}

.question-count {
  font-weight: 600;
  color: #764ba2;
}

.btn {
  padding: 12px 32px;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  margin-top: 24px;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.btn-start,
.btn-restart {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

.btn-next {
  background: #333;
  color: white;
  width: 100%;
}

What does this stylesheet do?

  • The body styles create a purple gradient background and center the app card on the page.
  • The .app class creates a white card with rounded corners and a shadow, giving the quiz a floating card appearance.
  • The .btn styles add a subtle lift effect on hover, making buttons feel interactive.
  • Check your browser at http://localhost:5173.

You should see a white card centered on a purple gradient background, with "React Quiz" as the heading, a description, and a purple "Start Quiz" button.

Seeing unstyled text or the old Vite page?

  • Make sure src/main.jsx imports './App.css' and not './index.css'.
  • Check that src/App.css exists and contains the styles shown above.
  • Delete the old src/index.css file if it's still there. Its styles may conflict with your new ones.

Help me debug why my StartScreen styles aren't showing up.

✔️ Awesome, I've got everything!

Great. Double check you've saved all your files before moving on.

ⓧ I'd like to double check the full code

Here's what each file should look like at this point.

function StartScreen({ onStart }) {
  return (
    <div className="start-screen">
      <h1>React Quiz</h1>
      <p>Test your knowledge of React fundamentals!</p>
      <p className="question-count">5 questions</p>
      <button className="btn btn-start" onClick={onStart}>
        Start Quiz
      </button>
    </div>
  );
}

export default StartScreen;
import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import questions from './data/questions.js';

function App() {
  const [stage, setStage] = useState('start');
  const [finalScore, setFinalScore] = useState(0);

  const handleStart = () => setStage('quiz');

  const handleFinish = (score) => {
    setFinalScore(score);
    setStage('result');
  };

  const handleRestart = () => {
    setFinalScore(0);
    setStage('start');
  };

  return (
    <div className="app">
      {stage === 'start' && <StartScreen onStart={handleStart} />}
      {stage === 'quiz' && <div>Quiz coming soon...</div>}
      {stage === 'result' && <div>Results coming soon...</div>}
    </div>
  );
}

export default App;
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './App.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app {
  background: white;
  border-radius: 16px;
  padding: 40px;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.start-screen,
.result-screen {
  text-align: center;
}

.start-screen h1,
.result-screen h1 {
  font-size: 2rem;
  color: #333;
  margin-bottom: 12px;
}

.start-screen p {
  color: #666;
  margin-bottom: 8px;
}

.question-count {
  font-weight: 600;
  color: #764ba2;
}

.btn {
  padding: 12px 32px;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  margin-top: 24px;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.btn-start,
.btn-restart {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

.btn-next {
  background: #333;
  color: white;
  width: 100%;
}
Test the screen transition

The whole point of the App shell is to swap between screens. Time to confirm that works.

  • In your browser, click the Start Quiz button.

The start screen disappears and you should see "Quiz coming soon..." in its place.

What just happened?

Clicking the button called handleStart, which set stage to 'quiz'.

When state changes, React re-renders the component. Since stage is now 'quiz', the condition stage === 'start' is false, so StartScreen unmounts and the placeholder div renders instead.

Nothing happens when you click?

  • Check that StartScreen.jsx has onClick={onStart} on the button element.
  • In App.jsx, make sure you're passing onStart={handleStart} as a prop to <StartScreen>.
  • Open the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for error messages.

My Start Quiz button isn't working. Help me debug the click handler.

Your app shell is wired up and the first screen looks great. Next up, you'll build the QuizScreen that displays questions, tracks the score, and gives immediate feedback on answers.

Build the Quiz Flow with State and Props

Your start screen is working and clicking the button transitions the app to the quiz stage.

Now you need to build the actual quiz experience. The QuizScreen component needs to track which question the learner is on, whether they've answered, and their running score. It also needs to communicate the final score back up to App when the quiz ends.

In this step, get ready to:
  • Build the QuizScreen component with state tracking and answer selection.
  • Add visual feedback that highlights correct and incorrect answers.
  • Wire QuizScreen into App so the full quiz flow works end-to-end.
Build the QuizScreen component

The QuizScreen component is the heart of your app. It needs three pieces of state: which question is showing, what the user selected, and the running score.

  • Create a new file at src/components/QuizScreen.jsx and add the following code:
import { useState } from 'react';
import questions from '../data/questions.js';

function QuizScreen({ onFinish }) {
  // Track which question we're on, the score, and what the user picked
  const [currentIndex, setCurrentIndex] = useState(0);
  const [score, setScore] = useState(0);
  const [selectedAnswer, setSelectedAnswer] = useState(null);

  // Get the current question object from the array
  const current = questions[currentIndex];

  // Handle when a user clicks an answer option
  const handleSelect = (option) => {
    if (selectedAnswer !== null) return;
    setSelectedAnswer(option);
    if (option === current.answer) {
      setScore((prev) => prev + 1);
    }
  };

  // Advance to the next question or finish the quiz
  const handleNext = () => {
    if (currentIndex + 1 < questions.length) {
      setCurrentIndex((prev) => prev + 1);
      setSelectedAnswer(null);
    } else {
      onFinish(score + (selectedAnswer === current.answer ? 0 : 0));
    }
  };

  // Return the correct CSS class based on answer state
  const getOptionClass = (option) => {
    if (selectedAnswer === null) return 'option';
    if (option === current.answer) return 'option correct';
    if (option === selectedAnswer) return 'option incorrect';
    return 'option';
  };

  return (
    <div className="quiz-screen">
      <div className="progress">
        Question {currentIndex + 1} of {questions.length}
      </div>
      <h2 className="question">{current.question}</h2>
      <div className="options">
        {current.options.map((option) => (
          <button
            key={option}
            className={getOptionClass(option)}
            onClick={() => handleSelect(option)}
            disabled={selectedAnswer !== null}
          >
            {option}
          </button>
        ))}
      </div>
      {selectedAnswer !== null && (
        <button className="btn btn-next" onClick={handleNext}>
          {currentIndex + 1 < questions.length ? 'Next Question' : 'See Results'}
        </button>
      )}
    </div>
  );
}

export default QuizScreen;

What does this code do?

This component manages the entire quiz flow using three state variables and three handler functions.

  • currentIndex tracks which question is displayed. score counts correct answers. selectedAnswer stores what the user clicked (or null if nothing yet).
  • handleSelect checks if the user already picked something (the early return prevents double-clicks). If the answer matches, it increments the score.
  • handleNext either advances to the next question (resetting selectedAnswer to null) or calls onFinish with the final score when all questions are done.
  • getOptionClass returns CSS class names that highlight the correct answer green and any wrong selection red. Before selection, all options just get the base option class.

What is conditional rendering?

The {selectedAnswer !== null && (...)} pattern only renders the Next button AFTER the user picks an answer. In React, you use JavaScript expressions inside JSX to show or hide elements based on state.

  • Save src/components/QuizScreen.jsx.

You won't see the quiz yet because App.jsx still renders a placeholder for the quiz stage. You'll connect it in a moment.

Seeing a red error in the browser?

  • Check that the import path says '../data/questions.js' with two dots. Components sit one folder deeper than src/, so you need to go up one level.
  • Make sure src/data/questions.js exports the array as export default questions.

Why is my QuizScreen import failing?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react';
import questions from '../data/questions.js';

function QuizScreen({ onFinish }) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [score, setScore] = useState(0);
  const [selectedAnswer, setSelectedAnswer] = useState(null);

  const current = questions[currentIndex];

  const handleSelect = (option) => {
    if (selectedAnswer !== null) return;
    setSelectedAnswer(option);
    if (option === current.answer) {
      setScore((prev) => prev + 1);
    }
  };

  const handleNext = () => {
    if (currentIndex + 1 < questions.length) {
      setCurrentIndex((prev) => prev + 1);
      setSelectedAnswer(null);
    } else {
      onFinish(score + (selectedAnswer === current.answer ? 0 : 0));
    }
  };

  const getOptionClass = (option) => {
    if (selectedAnswer === null) return 'option';
    if (option === current.answer) return 'option correct';
    if (option === selectedAnswer) return 'option incorrect';
    return 'option';
  };

  return (
    <div className="quiz-screen">
      <div className="progress">
        Question {currentIndex + 1} of {questions.length}
      </div>
      <h2 className="question">{current.question}</h2>
      <div className="options">
        {current.options.map((option) => (
          <button
            key={option}
            className={getOptionClass(option)}
            onClick={() => handleSelect(option)}
            disabled={selectedAnswer !== null}
          >
            {option}
          </button>
        ))}
      </div>
      {selectedAnswer !== null && (
        <button className="btn btn-next" onClick={handleNext}>
          {currentIndex + 1 < questions.length ? 'Next Question' : 'See Results'}
        </button>
      )}
    </div>
  );
}

export default QuizScreen;
Add the quiz styles for answer feedback

The getOptionClass function returns class names like option correct and option incorrect. You need CSS rules that make those classes visually distinct.

  • Open src/App.css and add the following styles at the bottom of the file:
.progress {
  text-align: center;
  color: #764ba2;
  font-weight: 600;
  margin-bottom: 20px;
}

.question {
  font-size: 1.2rem;
  color: #333;
  margin-bottom: 24px;
  line-height: 1.4;
}

.options {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.option {
  padding: 14px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  background: white;
  font-size: 1rem;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.2s, background 0.2s;
}

.option:hover:not(:disabled) {
  border-color: #764ba2;
  background: #f8f4ff;
}

.option:disabled {
  cursor: not-allowed;
}

.option.correct {
  border-color: #4caf50;
  background: #e8f5e9;
  color: #2e7d32;
}

.option.incorrect {
  border-color: #f44336;
  background: #ffebee;
  color: #c62828;
}

What do these styles do?

  • .progress centers the "Question 1 of 5" counter and gives it the purple accent color.
  • .option gives each answer button a bordered card look with a hover effect that only fires when the button is not disabled.
  • .option.correct turns the correct answer green. .option.incorrect turns the selected wrong answer red. These classes are applied by getOptionClass after the user picks.
  • Save src/App.css.

The styles are ready, but you still need to connect QuizScreen to the app before you can see them in action.

Styles not applying later?

  • Make sure there are no typos in the class names. The CSS uses .option.correct (no space between), which targets an element with BOTH classes.
  • Check that the new styles are placed AFTER the existing .btn-next rule, not accidentally nested inside another selector.

Help me debug why my quiz option styles aren't showing up.

✔️ Awesome, I've got everything!

Great. Make sure you saved the file before continuing.

ⓧ I'd like to double check the full code

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app {
  background: white;
  border-radius: 16px;
  padding: 40px;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.start-screen,
.result-screen {
  text-align: center;
}

.start-screen h1,
.result-screen h1 {
  font-size: 2rem;
  color: #333;
  margin-bottom: 12px;
}

.start-screen p {
  color: #666;
  margin-bottom: 8px;
}

.question-count {
  font-weight: 600;
  color: #764ba2;
}

.btn {
  padding: 12px 32px;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  margin-top: 24px;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.btn-start,
.btn-restart {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

.btn-next {
  background: #333;
  color: white;
  width: 100%;
}

.progress {
  text-align: center;
  color: #764ba2;
  font-weight: 600;
  margin-bottom: 20px;
}

.question {
  font-size: 1.2rem;
  color: #333;
  margin-bottom: 24px;
  line-height: 1.4;
}

.options {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.option {
  padding: 14px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  background: white;
  font-size: 1rem;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.2s, background 0.2s;
}

.option:hover:not(:disabled) {
  border-color: #764ba2;
  background: #f8f4ff;
}

.option:disabled {
  cursor: not-allowed;
}

.option.correct {
  border-color: #4caf50;
  background: #e8f5e9;
  color: #2e7d32;
}

.option.incorrect {
  border-color: #f44336;
  background: #ffebee;
  color: #c62828;
}
Connect QuizScreen to App and test the flow

Right now, App.jsx renders a placeholder div when the stage is 'quiz'. You need to swap that placeholder for the real QuizScreen component and pass it the onFinish prop.

  • In src/App.jsx, find this import line near the top:
import StartScreen from './components/StartScreen.jsx';
  • Add a new import for QuizScreen directly below it:
import QuizScreen from './components/QuizScreen.jsx';
  • Now find this line inside the return statement:
{stage === 'quiz' && <div>Quiz coming soon...</div>}
  • Replace it with this line that renders QuizScreen and passes handleFinish as the onFinish prop:
{stage === 'quiz' && <QuizScreen onFinish={handleFinish} />}

How do props connect these components?

When QuizScreen finishes all questions, it calls onFinish(score). Because you passed handleFinish as that prop, calling onFinish inside QuizScreen actually runs handleFinish inside App.

This is how child components communicate back to parents in React. Data flows down via props, and events flow up via callback functions passed as props.

  • Save src/App.jsx.
  • Switch back to your browser at http://localhost:5173.
  • Click Start Quiz.
  • You should see "Question 1 of 5" with the first question text and four option buttons.
  • Click an answer. The correct one turns green, a wrong one turns red, and a Next Question button appears below.
  • Click through all 5 questions. After the last one, clicking See Results should show the placeholder text "Results coming soon...".

Quiz not appearing after clicking Start?

  • Make sure you saved both App.jsx and QuizScreen.jsx. Vite only picks up changes in saved files.
  • Check the browser console for import errors. The import path in App should be './components/QuizScreen.jsx' with a single dot (App.jsx lives in src/, same level as the components/ folder).
  • Verify that QuizScreen is spelled exactly the same in the import and in the JSX. Component names are case-sensitive.

Help me debug why my QuizScreen isn't rendering after I click Start Quiz.

✔️ Awesome, I've got everything!

Great. Confirm you've saved App.jsx before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import QuizScreen from './components/QuizScreen.jsx';
import questions from './data/questions.js';

function App() {
  const [stage, setStage] = useState('start');
  const [finalScore, setFinalScore] = useState(0);

  const handleStart = () => setStage('quiz');

  const handleFinish = (score) => {
    setFinalScore(score);
    setStage('result');
  };

  const handleRestart = () => {
    setFinalScore(0);
    setStage('start');
  };

  return (
    <div className="app">
      {stage === 'start' && <StartScreen onStart={handleStart} />}
      {stage === 'quiz' && <QuizScreen onFinish={handleFinish} />}
      {stage === 'result' && <div>Results coming soon...</div>}
    </div>
  );
}

export default App;

Your quiz flow is fully functional. You can start the quiz, answer all five questions with immediate green/red feedback, and advance through to the end. Next up, you'll build the Results screen so the learner sees their final score instead of a placeholder.

Build the Results Screen and Polish

Your quiz flow is working. Learners can start the quiz, answer questions, and see feedback on each answer. The last piece of the puzzle is showing them how they did overall.

In this step, you'll build the ResultScreen component that displays the final score with a performance message. You'll also add some CSS polish and test the complete app from start to finish.

In this step, get ready to:
  • Build the ResultScreen component with score display and a performance message.
  • Add CSS styling for the score display.
  • Test the complete quiz flow end-to-end.
Build the ResultScreen component

The results screen is the final destination of the quiz. It needs to show how many questions the learner got right, give them a motivational message based on their performance, and offer a way to try again.

  • Create a new file at src/components/ResultScreen.jsx and add the following code:
function ResultScreen({ score, total, onRestart }) {
  // Calculate percentage from score and total
  const percentage = Math.round((score / total) * 100);

  // Choose a message based on performance
  let message;
  if (percentage === 100) {
    message = "Perfect score! You're a React expert! 🎉";
  } else if (percentage >= 60) {
    message = "Great job! You know your React basics! 👏";
  } else {
    message = "Keep learning! Practice makes perfect. 💪";
  }

  return (
    <div className="result-screen">
      <h1>Quiz Complete!</h1>
      <div className="score-display">
        <span className="score-number">{score}</span>
        <span className="score-divider">/</span>
        <span className="score-total">{total}</span>
      </div>
      <p className="score-percentage">{percentage}%</p>
      <p className="score-message">{message}</p>
      <button className="btn btn-restart" onClick={onRestart}>
        Play Again
      </button>
    </div>
  );
}

export default ResultScreen;

What does this code do?

This component accepts three props: score (how many the learner got right), total (total questions), and onRestart (a function to reset the quiz).

It calculates a percentage from the score, then uses conditional logic to pick a motivational message. The Play Again button calls onRestart to bring the learner back to the start screen.

Now you need to connect this component to App.jsx so it actually renders. Right now, the result stage shows a placeholder.

  • In src/App.jsx, find the import section at the top of the file:
import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import QuizScreen from './components/QuizScreen.jsx';
import questions from './data/questions.js';
  • Add the ResultScreen import so the section looks like this:
import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import QuizScreen from './components/QuizScreen.jsx';
import ResultScreen from './components/ResultScreen.jsx';
import questions from './data/questions.js';
  • Next, find the placeholder line in the return statement:
{stage === 'result' && <div>Results coming soon...</div>}
  • Replace that entire line with the ResultScreen render, so it looks like this:
{stage === 'result' && (
  <ResultScreen
    score={finalScore}
    total={questions.length}
    onRestart={handleRestart}
  />
)}

What are these props doing?

You're passing finalScore (tracked in App's state), questions.length (the total number of questions from your data file), and handleRestart (which resets the state back to the start screen).

  • Save both src/components/ResultScreen.jsx and src/App.jsx.
  • Switch to your browser at http://localhost:5173. Play through the quiz by clicking answers and advancing through all 5 questions.
  • After the last question, you should see a results screen showing your score as a fraction (like 4/5), a percentage, and a message.

Don't see the results screen?

  • Make sure you saved both files. The import in App.jsx must point to ./components/ResultScreen.jsx exactly.
  • Check that you fully replaced the placeholder line. No part of <div>Results coming soon...</div> should remain.
  • Look at the terminal running your dev server for any error messages about missing exports.

Why isn't my ResultScreen showing after the quiz?

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full ResultScreen.jsx:

function ResultScreen({ score, total, onRestart }) {
  const percentage = Math.round((score / total) * 100);

  let message;
  if (percentage === 100) {
    message = "Perfect score! You're a React expert! 🎉";
  } else if (percentage >= 60) {
    message = "Great job! You know your React basics! 👏";
  } else {
    message = "Keep learning! Practice makes perfect. 💪";
  }

  return (
    <div className="result-screen">
      <h1>Quiz Complete!</h1>
      <div className="score-display">
        <span className="score-number">{score}</span>
        <span className="score-divider">/</span>
        <span className="score-total">{total}</span>
      </div>
      <p className="score-percentage">{percentage}%</p>
      <p className="score-message">{message}</p>
      <button className="btn btn-restart" onClick={onRestart}>
        Play Again
      </button>
    </div>
  );
}

export default ResultScreen;

And here is the full App.jsx:

import { useState } from 'react';
import StartScreen from './components/StartScreen.jsx';
import QuizScreen from './components/QuizScreen.jsx';
import ResultScreen from './components/ResultScreen.jsx';
import questions from './data/questions.js';

function App() {
  const [stage, setStage] = useState('start');
  const [finalScore, setFinalScore] = useState(0);

  const handleStart = () => setStage('quiz');

  const handleFinish = (score) => {
    setFinalScore(score);
    setStage('result');
  };

  const handleRestart = () => {
    setFinalScore(0);
    setStage('start');
  };

  return (
    <div className="app">
      {stage === 'start' && <StartScreen onStart={handleStart} />}
      {stage === 'quiz' && <QuizScreen onFinish={handleFinish} />}
      {stage === 'result' && (
        <ResultScreen
          score={finalScore}
          total={questions.length}
          onRestart={handleRestart}
        />
      )}
    </div>
  );
}

export default App;
Add CSS styling for the score display

The results screen renders, but the score display could use some visual treatment. You'll add styles that make the score large and prominent, with a colored percentage below it.

  • Open src/App.css and add the following styles at the bottom of the file:
/* Score display on the results screen */
.score-display {
  font-size: 3rem;
  font-weight: 700;
  color: #333;
  margin: 20px 0 8px;
}

.score-divider {
  color: #ccc;
  margin: 0 4px;
}

.score-percentage {
  font-size: 1.5rem;
  color: #764ba2;
  font-weight: 600;
}

.score-message {
  color: #666;
  margin-top: 12px;
  font-size: 1.1rem;
}

What does this CSS do?

  • .score-display makes the fraction (like 4/5) large and bold so it's the first thing the learner sees.
  • .score-divider lightens the slash between the numbers so it doesn't compete with the score itself.
  • .score-percentage uses the same purple accent color from the rest of the app to keep things cohesive.
  • .score-message styles the motivational text in a softer gray so it supports the score without overwhelming it.
  • Save src/App.css.
  • Switch to your browser and play through the quiz again. On the results screen, the score should now appear large and bold, with the percentage in purple below it.

Score still looks unstyled?

  • Check that the class names in your CSS match your JSX exactly. For example, score-display in CSS must match className="score-display" in your component.
  • Make sure you added the styles at the bottom of App.css and did not accidentally nest them inside another rule.

Why aren't my CSS styles applying to the results screen?

✔️ Awesome, I've got everything!

Great. Your CSS file is complete.

ⓧ I'd like to double check the full code

Here is the full App.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app {
  background: white;
  border-radius: 16px;
  padding: 40px;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.start-screen,
.result-screen {
  text-align: center;
}

.start-screen h1,
.result-screen h1 {
  font-size: 2rem;
  color: #333;
  margin-bottom: 12px;
}

.start-screen p {
  color: #666;
  margin-bottom: 8px;
}

.question-count {
  font-weight: 600;
  color: #764ba2;
}

.btn {
  padding: 12px 32px;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  margin-top: 24px;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.btn-start,
.btn-restart {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

.btn-next {
  background: #333;
  color: white;
  width: 100%;
}

.progress {
  text-align: center;
  color: #764ba2;
  font-weight: 600;
  margin-bottom: 20px;
}

.question {
  font-size: 1.2rem;
  color: #333;
  margin-bottom: 24px;
  line-height: 1.4;
}

.options {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.option {
  padding: 14px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  background: white;
  font-size: 1rem;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.2s, background 0.2s;
}

.option:hover:not(:disabled) {
  border-color: #764ba2;
  background: #f8f4ff;
}

.option:disabled {
  cursor: not-allowed;
}

.option.correct {
  border-color: #4caf50;
  background: #e8f5e9;
  color: #2e7d32;
}

.option.incorrect {
  border-color: #f44336;
  background: #ffebee;
  color: #c62828;
}

.score-display {
  font-size: 3rem;
  font-weight: 700;
  color: #333;
  margin: 20px 0 8px;
}

.score-divider {
  color: #ccc;
  margin: 0 4px;
}

.score-percentage {
  font-size: 1.5rem;
  color: #764ba2;
  font-weight: 600;
}

.score-message {
  color: #666;
  margin-top: 12px;
  font-size: 1.1rem;
}
Test the complete flow end-to-end

All the pieces are in place. Time to verify that the entire app works as a cohesive experience from start to finish.

  • Switch to your browser at http://localhost:5173.
  • Click Start Quiz on the start screen.
  • Answer all 5 questions. Notice how correct answers highlight green and incorrect answers highlight red.
  • After the last question, click See Results.
  • Confirm you see your score as a fraction, a percentage, and a performance message.
  • Click Play Again and confirm you're taken back to the start screen.
  • Start a new quiz and verify the score resets (it does not carry over from the previous round).

What should I see?

The results screen shows a bold score like 4/5, a purple percentage like 80%, and a message like "Great job! You know your React basics!" depending on how well you did.

The Play Again button has the same purple gradient as the Start Quiz button, and it lifts slightly on hover.

Play Again not resetting properly?

  • If the quiz starts from where you left off instead of question 1, confirm that handleRestart in App.jsx sets stage back to 'start'. The QuizScreen component re-mounts fresh each time because React destroys and recreates it when the conditional render toggles.
  • If the score carries over, check that handleRestart also sets finalScore back to 0.

Help me debug why Play Again isn't resetting my quiz properly.

Your quiz app is fully functional. You've built a complete React application with multiple screens, state management, props flowing between components, and conditional rendering driving the navigation. Next up, the Secret Mission challenges you to add a countdown timer to each question.

Secret mission

Add a Countdown Timer

Your quiz works great, but there's no pressure. Add a 15-second countdown timer to each question that auto-advances when time runs out. This introduces React's useEffect hook for managing side effects like intervals.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The react-quiz project folder on your Desktop (contains all source code and node_modules).
  • The Vite dev server process (running in your terminal).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep experimenting with your quiz app.

  • Your dev server stays running at http://localhost:5173.
  • Your project files stay on your Desktop in the react-quiz folder.
  • You can restart the dev server at any time by running npm run dev from the react-quiz folder.

Pause - I'll come back to this later

Shut down the running dev server to free up your terminal. Your project files stay on disk so you can resume later.

  • Switch to the terminal where the dev server is running.
  • Press Ctrl+C to stop the server.
  • When you want to resume, open a terminal in your react-quiz folder and run npm run dev again.

Delete - I don't want to use this again

Remove all project files and stop the dev server. This frees up disk space and fully cleans up.

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

On macOS/Linux:

cd ~/Desktop
rm -rf react-quiz

On Windows:

cd ~\Desktop
rmdir /s /q react-quiz
  • Confirm the folder is gone by checking your Desktop. You should no longer see a react-quiz folder.

Nice Work!

Nice Work!

You just built a complete React app from scratch. From planning the architecture in Claude Code to wiring up components with state and props, you now have a working interactive quiz running locally on your machine.

You've learned how to:

  • Build a multi-component React app with Vite as your build tool, using props to pass data between components and conditional rendering to show different screens.
  • Use useState to manage application state across multiple screens, tracking score, current question, and app stage.
  • Use Claude Code's Plan Mode to architect an app before building it, separating data from UI logic for clean architecture.
  • Added a countdown timer using useEffect for side-effect management.

Ready to quiz yourself?