React Quiz Component
ExamplesHeadless

Provider and context

Share one quiz session across separate question, timer, navigation, and results components using QuizProvider and useQuizContext. Try pausing the timer, revisiting questions, and filtering results, then explore the complete implementation and quiz data.

Try the quiz

Review answers with previous/next, pause the timer, submit, and filter results.

Implementation

QuizProvider and useQuizContext provide quiz state and actions without rendering or styling an interface. This example builds a custom UI. For a complete styled quiz with no custom CSS, use the default Quiz component.

Install react-quiz-component, then copy this component and the quiz.ts data file into the same directory. The preview mounts in the browser; Next.js applications should use a client boundary.

provider.tsx
'use client';

import './examples.css';
import {
  QuizProvider,
  useQuizContext,
  type ResultFilter,
} from 'react-quiz-component';
import { quiz } from './quiz';

function Question() {
  const { state, actions } = useQuizContext();
  if (state.status === 'invalid')
    return <p role="alert">{state.errors.join(' ')}</p>;
  if (state.status === 'idle')
    return (
      <>
        <h2>{quiz.quizTitle}</h2>
        <p>Review your answers before submitting. You have two minutes.</p>
        <button onClick={actions.start}>Start quiz</button>
      </>
    );
  if (state.status === 'paused') return <output>Quiz paused.</output>;
  if (state.status !== 'active') return null;
  return (
    <>
      <p>
        Question {state.currentQuestionIndex + 1} of {state.questions.length}
      </p>
      <h2>{state.currentQuestion?.question}</h2>
      <div className="answers">
        {state.answers.map((answer) => (
          <button
            key={answer.answerNumber}
            disabled={answer.disabled}
            aria-pressed={answer.selected}
            onClick={() => actions.selectAnswer(answer.answerNumber)}
          >
            {answer.value}
          </button>
        ))}
      </div>
    </>
  );
}

function Timer() {
  const { state, actions } = useQuizContext();
  if (state.status !== 'active' && state.status !== 'paused') return null;
  return (
    <div>
      <p>Time remaining: {state.timeRemaining}s</p>
      {state.canPause && (
        <button onClick={actions.pauseTimer}>Pause timer</button>
      )}
      {state.canResume && (
        <button onClick={actions.resumeTimer}>Resume timer</button>
      )}
    </div>
  );
}

function Navigation() {
  const { state, actions } = useQuizContext();
  return (
    <div>
      {state.canGoPrevious && (
        <button onClick={actions.previousQuestion}>Previous question</button>
      )}
      {state.canGoNext && (
        <button onClick={actions.nextQuestion}>Next question</button>
      )}
      {state.canSubmit && (
        <button onClick={actions.submitQuiz}>Finish quiz</button>
      )}
    </div>
  );
}

function Results() {
  const { state, actions } = useQuizContext();
  if (state.status !== 'completed') return null;
  return (
    <>
      <output>
        Score: {state.summary.correctPoints} / {state.summary.totalPoints}
      </output>
      <label>
        Filter results{' '}
        <select
          value={state.resultFilter}
          onChange={(event) =>
            actions.setResultFilter(event.target.value as ResultFilter)
          }
        >
          <option value="all">All</option>
          <option value="correct">Correct</option>
          <option value="incorrect">Incorrect</option>
          <option value="unanswered">Unanswered</option>
        </select>
      </label>
      <ul>
        {state.filteredResults.map((result) => (
          <li key={result.index}>
            {result.question.question}:{' '}
            {result.answered ? result.outcome : 'unanswered'}
          </li>
        ))}
      </ul>
    </>
  );
}

export default function ProviderExample() {
  return (
    <QuizProvider
      quiz={quiz}
      allowNavigation
      revealAnswerOnSubmit
      timer={120}
      allowPauseTimer
    >
      <section className="headless-demo" aria-label="Shared quiz">
        <Timer />
        <Question />
        <Results />
        <Navigation />
      </section>
    </QuizProvider>
  );
}

Custom styles

Save this stylesheet as examples.css alongside the component. The implementation imports it to style the layout, answer buttons, result filter, and keyboard focus. The CSS uses the documentation theme colors when available, with fallback colors for your own application.

examples.css
.headless-demo {
  display: grid;
  gap: 16px;
}
.headless-demo h2 {
  font-size: 1.2rem;
  font-weight: 600;
}
.headless-demo .answers {
  display: grid;
  gap: 8px;
}
.headless-demo .question-group {
  display: grid;
  min-inline-size: 0;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--color-fd-border, #d4d4d8);
  border-radius: 8px;
}
.headless-demo .question-group legend {
  max-inline-size: 100%;
  padding-inline: 4px;
  font-weight: 600;
}
.headless-demo button,
.headless-demo select {
  border: 1px solid var(--color-fd-border, #d4d4d8);
  border-radius: 8px;
  padding: 8px 12px;
  margin: 4px 8px 4px 0;
  cursor: pointer;
  background: var(--color-fd-background, #fff);
  color: var(--color-fd-foreground, #18181b);
}
.headless-demo button:hover {
  background: var(--color-fd-muted, #f4f4f5);
}
.headless-demo button[aria-pressed='true'] {
  border-color: var(--color-fd-primary, #2563eb);
  background: var(--color-fd-accent, #eff6ff);
}
.headless-demo button:disabled {
  opacity: 0.65;
  cursor: default;
}
.headless-demo :focus-visible {
  outline: 2px solid var(--color-fd-primary, #2563eb);
  outline-offset: 3px;
}

Quiz data

quiz.ts
import type { Quiz } from 'react-quiz-component';

export const quiz: Quiz = {
  quizTitle: 'React essentials',
  quizSynopsis: 'Two questions about React hooks and rendering.',
  questions: [
    {
      question: 'Which hook stores local state?',
      questionType: 'text',
      answerSelectionType: 'single',
      answers: ['useState', 'useEffect', 'useRef'],
      correctAnswer: '1',
      point: 10,
      messageForCorrectAnswer: 'Correct! useState stores component state.',
      messageForIncorrectAnswer: 'useState is the hook for local state.',
      explanation: 'useState returns the current value and a setter.',
    },
    {
      question: 'Which APIs render React content? Pick two.',
      questionType: 'text',
      answerSelectionType: 'multiple',
      answers: ['createRoot', 'renderToString', 'querySelector'],
      correctAnswer: [1, 2],
      point: 20,
      explanation:
        'createRoot renders in the browser; renderToString renders HTML on the server.',
    },
  ],
};

On this page