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.
'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.
.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
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.',
},
],
};
Standalone hook
Build a custom quiz interface around a standalone useQuiz session. Try starting the quiz, selecting answers, and viewing the final score, then explore the runnable source and quiz data behind the preview.
All questions and review
Show every question together or paginate a quiz, edit selections, review answers, and submit once using the existing headless API.