Quiz

Quiz checks understanding with one-answer and open-ended questions inside a lesson, then shows a score. Use it at the end of a lesson section.

Follows the live learner app (owner, 2026-10-14), restyled with Nuton tokens. Beta until design adds the live quiz to Figma and confirms the deviations listed under States.

Preview

Choose an option: it answers straight away. Use Next, Back and View results to go through the whole flow. Change the width to see the phone sizes.

Question 1 of 3

What best defines Human-Centered Design?

State: question 1, 0 answered, score 0

tsx
import { Quiz } from '@nuton/ui'

const questions = [
  {
    question: 'What best defines Human-Centered Design?',
    options: ['A process focused on aesthetics', 'A design approach that prioritizes user needs', …],
    correctAnswer: 1,
    explanation: 'Human-Centered Design starts from people’s goals…',
  },
  …
]

<Quiz
  questions={questions}
  defaultState={savedState}
  onStateChange={save}
/>

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

Usage

tsx
import { Quiz, type QuizState } from '@nuton/ui'

const questions = [
  {
    question: 'What best defines Human-Centered Design?',
    options: ['A process focused on aesthetics', 'A design approach that prioritizes user needs'],
    correctAnswer: 1,
    explanation: 'Human-Centered Design starts from people’s goals and behaviours.',
  },
]

<Quiz
  questions={questions}
  defaultState={savedState}          // restore the learner's progress
  onStateChange={(state) => save(state)}
  saveStatus={saveStatus}            // 'idle' | 'saving' | 'saved' | 'failed'
/>
  • Use Quiz for the whole flow. It handles answering, locking, feedback, Back and Next, and the results screen.
  • Clicking an option answers the question. There is no separate check step, so write options that are clearly different.
  • Add an explanation to every question. It turns a wrong answer into learning.
  • Save progress with onStateChange and show the request with saveStatus. Restore it with defaultState, or control it with state.
  • Add type: 'open' for a written answer. Give it a modelAnswer. Pass evaluate to grade answers in your app or with AI; without it, learners compare with the model answer and mark themselves.
  • Use QuizOption or QuizResults on their own only for custom layouts.

Variants and sizes

Quiz has no visual variants. It fills its container and switches to the live phone sizes when the container is narrower than 480px (a container query, so it also works in side panels).

Lesson column

Question 1 of 3

What best defines Human-Centered Design?

Narrow container

Question 1 of 3

What best defines Human-Centered Design?

PartLesson columnNarrow (< 480px)
CardRadius 20, 1px Stroke/Soft-200, Bg/White; padding 24 top, 28 sides, 24 bottomPadding 20 top and bottom, 16 sides
Header“Question n of N” Label/Small Sub-600; save status Label/X SmallSame
ProgressProgress, 3px, 12 above and 24 belowSame
QuestionTitle/H6 (Fraunces 20/28), 24 belowFraunces 17/24, 16 below
OptionsGap 12; padding 14/16, gap 14, radius 12; circle 26; Paragraph/MediumGap 10; padding 12, gap 12; circle 22; Paragraph/Small
FeedbackIcon + Label/Large title, 12 below; explanation padding 20, radius 12, Bg/Weak-50Same
FooterBorder-top, 16 above; Back (ghost) and Next (filled pill), Button sm 36pxSame
ResultsPadding 40/28; Title/H1 percent; 200px bar; Retake Button sm lighterPadding 32/16

States and interactions

Quiz states

Question 1 of 3

What best defines Human-Centered Design?

Question 2 of 3

Why is empathy a core principle in Human-Centered Design?

Question 1 of 1

Not saved — retrying on your next answer

What best defines Human-Centered Design?

saveStatusTextColour
idleNothing—
savingSaving…Text/Sub-600
savedSavedText/Sub-600
failedNot saved — retrying on your next answerred-700

Results tiers

80%

4 of 5 correct

You have a strong grasp of this material.

80% and above“You have a strong grasp of this material.”

60%

3 of 5 correct

Solid effort. Reviewing the chapter could strengthen your understanding.

50% to 79%“Solid effort. Reviewing the chapter could strengthen your understanding.”

20%

1 of 5 correct

A re-read of the chapter will help. Give it another go.

Below 50%“A re-read of the chapter will help. Give it another go.”

Open-ended questions

Figma Quiz 292:13104 has an “Open ended” variant (3009:42182): the question, then a Text Area with “Type your answer here…”. The live quiz has no open questions yet, so marking, loading and error states are code-only. Check answer locks the answer (read-only, still focusable), then it is marked in one of two ways.

Question 1 of 1

In your own words, why does empathy matter in Human-Centered Design?

Two or three sentences is enough.

Question 1 of 1

In your own words, why does empathy matter in Human-Centered Design?

Two or three sentences is enough.

Question 1 of 1

In your own words, why does empathy matter in Human-Centered Design?

Two or three sentences is enough.

Question 1 of 1

In your own words, why does empathy matter in Human-Centered Design?

Revealed, not markedAnswer locked; model answer and explanation shown; “I got it” and “Not quite”. Next stays disabled until the learner marks it.

Question 1 of 1

In your own words, why does empathy matter in Human-Centered Design?

Partly rightOrange-800 icon and “Partly right”, the grader's feedback, then the model answer. Scores 0.5 by default.
StepWith evaluateWithout (self-assessment)
WritingTextarea named by the question, placeholder, optional guidance and counter. Empty or shorter than minLength: an error under the field, focus stays in it.
Check answerAnswer locks; the button shows a spinner and “Checking your answer…”; announced politely.Answer locks; model answer and explanation appear; focus moves to the locked answer.
MarkedVerdict (Correct! / Partly right / Wrong!), feedback, model answer, explanation.“I got it” → You got it (1). “Not quite” → Not quite (0).
ErrorRed-700 message and Try again in place of the button (focus stays on it). The answer stays locked.—

Scoring

Multiple choice scores 1 or 0. Open answers score correct 1, partial 0.5 and incorrect 0. Change this with scoring, e.g. scoring={{ partial: 0.25 }}. The results screen shows the total, e.g. “2.5 of 3 correct”. quizScore(questions, answers, scoring) gives the same number outside the component.

Option states

IdleStroke/Soft-200, Bg/White; circle with a 1.5px Stroke/Sub-300 ring. Hover: Stroke/Sub-300 and Bg/Weak-50.
Focus (code-only)State/Primary/Base border and the primary focus halo.
Correct answerGreen-700 border and circle with a check, State/Success/Lighter, green-800 text and “Correct answer” when the learner chose another option.
Your wrong answerState/Error/Base border, red-700 circle with a cross, State/Error/Lighter, red-700 text and “Your answer”.
DimmedOther options after answering: Text/Sub-600 and a Soft-200 ring. No opacity, so the text stays readable.
Disabled (code-only)Not used in the live flow. Text/Disabled-500 and a not-allowed cursor.

Answering a question

  1. Next is disabled until the question is answered.
  2. Clicking an option (or Enter or Space on it) answers. “Correct!” or “Wrong! The correct answer is B.” is announced politely.
  3. All options lock. The right option and the learner's wrong pick get an icon and a caption; the others are dimmed.
  4. The feedback title and explanation appear, and the progress bar moves on.
  5. Back returns to answered questions, which stay locked. The score counts each question once. Unsent drafts of open answers are kept while the quiz is open.
  6. On the last question the button says View results. Retake starts again from question 1.

State tokens

Set on .nu-quiz-option: --nu-quiz-option-bg, -stroke, -fg, -marker-stroke, -marker-bg, -marker-fg, -caption, -radius, -marker-size and -focus-shadow. On .nu-quiz: --nu-quiz-radius, -pad-top, -pad-inline, -pad-bottom, -question-size and -options-gap.

Where code differs from the live app

  • Grey #a4a4a4 text (“Question n of N”, save status, Back, option letters, the results message and “%”) is 2.49:1 on white. Code uses Text/Sub-600.
  • Other options are dimmed with opacity .4 on live. Code uses Sub-600 text and a soft ring, so they stay readable.
  • “🎉 Correct!” and “❌ Wrong!” use emoji. Code uses a check or cross icon in green-700 or red-700, so screen readers don't read “party popper”.
  • The live progress track (#f7f7f7) is almost invisible on white. Code uses the Progress track (Bg/Soft-200).
  • Raw sizes map to the nearest Nuton style: option text 15/22 → Paragraph/Medium 16/28; header 13 → Label/Small 14; save status 11 → Label/X Small 12; feedback title 17 → Label/Large 18; score 15 → Label/Medium 16; message 13 → Paragraph/Small 14; radius 14 → 12. Phone question size 17/24 is kept as a component token.
  • Next, View results and Retake are Nuton Buttons (sm, 36px, Label/Small): disabled uses the Button disabled colours, Retake is the lighter style (Bg/Soft-100). Live copy “View Results” and “Retake Quiz” is sentence case.
  • Motion uses Nuton durations: 200ms for the question (live 180ms), 300ms fade for the feedback (live animates height over 250ms). The results bar doesn't animate from 0. All motion is off with reduced motion.
  • Code adds: a polite announcement with the right answer, screen-reader text on options (“Your answer, correct”), focus management, a focus ring and a disabled option.

Composition examples

In a lesson, with saved progress

The live lesson puts a “QUIZ · 4” label and a hatched rule above the card. That label is page markup, not part of Quiz; here it is hidden from screen readers because the quiz region already has a name. Each change is “saved” with a short delay to show the save status.

Question 1 of 4

What best defines Human-Centered Design?

API reference

Quiz props

Quiz props
PropTypeDefaultDescription
questionsrequiredQuizQuestionData[]—Multiple choice: { question, options, correctAnswer, explanation?, id? }. Open: { type: 'open', question, modelAnswer?, explanation?, placeholder?, guidance?, minLength?, maxLength?, rows?, id? }. Content fields accept rich content.
stateQuizState—Controlled state: { currentIndex, answers, score, done }, the live savedState shape. answers maps question index to an option index, or to { text, result?, feedback? } for open questions.
defaultStateQuizStateinitialQuizStateInitial state when uncontrolled, e.g. saved progress.
onStateChange(state: QuizState) => void—Called after every answer, Back, Next, View results and Retake.
onAnswer(detail: { questionIndex, optionIndex, correct }) => void—Called once per multiple-choice question, when the learner answers.
evaluate(answer, question, { questionIndex, signal }) => Promise<{ result, feedback? }>—Grades open answers ('correct' | 'partial' | 'incorrect'). Shows a loading state, then the verdict; a rejected promise shows an error with Try again. The signal aborts on Retake and unmount. Without it, learners self-assess.
onOpenAnswer(detail: { questionIndex, text, result, markedBy: 'evaluate' | 'self' }) => void—Called when an open answer is marked.
scoringPartial<{ correct, partial, incorrect }>{ correct: 1, partial: 0.5, incorrect: 0 }Points per open-answer result.
onComplete(state: QuizState) => void—Called when the learner opens the results screen.
onRetake() => void—Called when the learner presses Retake. The state resets to question 1 either way.
saveStatus'idle' | 'saving' | 'saved' | 'failed''idle'Save status in the header, announced politely.
mascotbooleantrueNuri beside the verdict instead of the icon: happy when right, sad when wrong (encouraging for a partly right open answer), with the accent repeating. Also on the score card. false keeps the icons and a text-only card.
labelsPartial<QuizLabels>—Overrides for every string: name, position(n, total), progressBar, progressValue, saving, saved, saveFailed, correct, incorrect, correctAnswerIs(letter), explanation, back, next, viewResults, letter(index), the option captions, the results texts, and for open questions answerLabel, answerPlaceholder, submit, grading, gradeError, retry, answerRequired, answerTooShort(min), characterCount(count, max), partial, modelAnswer, selfAssessPrompt, selfCorrect, selfIncorrect, selfMarkedCorrect, selfMarkedIncorrect, answerRevealed.
aria-label / aria-labelledbystring'Quiz'Names the quiz region. Name each quiz when a page has more than one.
…section propsHTMLAttributes<HTMLElement>—className and other attributes go to the section. The ref points at it.

QuizOption props

QuizOption props
PropTypeDefaultDescription
letterrequiredstring—Letter in the circle. Always part of the name, also when an icon replaces it.
childrenrequiredReactNode—Option content.
result'correct' | 'incorrect'—Marks the option after answering with colour, an icon and a caption.
chosenbooleanfalseWhether this is the learner's answer. Picks the caption.
lockedbooleanfalseAfter answering: aria-disabled, clicks ignored, still focusable. Without result it looks dimmed.
labelsPartial<QuizOptionLabels>—correctAnswer, yourAnswer, yourAnswerCorrect, yourAnswerIncorrect.
…button propsButtonHTMLAttributes—onClick, disabled, tabIndex and others go to the button. The ref points at it.

QuizResults props

QuizResults props
PropTypeDefaultDescription
scorerequirednumber—Correct answers.
totalrequirednumber—Number of questions.
onRetake() => void—Shows the Retake button.
mascotbooleantrueNuri above the score: celebrating at 80% and up, proud at 50% and up, encouraging below. false leaves the card text only.
labelsPartial<QuizResultsLabels>—resultsTitle, score(score, total), messageHigh, messageMedium, messageLow, retake.
…div propsHTMLAttributes<HTMLDivElement>—Passed to the results group. The ref points at it.

Accessibility

Keyboard

KeyBehaviour
TabMoves through the options, then Back and Next. After answering only the chosen option stays in the tab order
Enter / SpaceAnswers with the focused option, or presses Check answer, I got it, Not quite, Back, Next, View results or Retake
TabIn an open answer, Tab and Shift+Tab leave the textarea (Tab is not captured); Enter adds a new line

Screen readers

The quiz is a region named “Quiz” (or your aria-label). Options are buttons in a group named by the question; each name starts with its letter (“B, Mitochondria”). After answering, “Correct!” or “Wrong! The correct answer is B.” is announced politely, and option names end with “Correct answer”, “Your answer, correct” or “Your answer, incorrect”. Locked options are aria-disabled. The save status is a polite status. The progress bar is named “Quiz progress”. Open answers: the textarea is named by the question, is required, and is described by the guidance, counter and any error. “Checking your answer…”, the verdict with feedback, errors and “Answer locked…” are announced politely; the open verdict stays readable later (it is not hidden).

Focus

After answering, focus stays on the chosen option, because options lock with aria-disabled rather than disabled. Back, Next and Retake move focus to the new question; View results moves it to the results group. Options show a primary border and halo. Open answers: an empty Check keeps focus in the textarea; while grading, focus stays on the busy button (and on Try again after an error); when the button goes away, focus moves to the locked, read-only answer.

Contrast and colour

All text is at least Text/Sub-600 (live #a4a4a4 fails at 2.49:1); dimmed options use Sub-600, not opacity. Correct and wrong use green-800 and red-700 text, and an icon plus a caption or screen-reader text, so nothing relies on colour alone (WCAG 1.4.1). Borders are at least 3:1. Forced colours keep borders, icons and focus.

Touch and pointer

The whole option is the target, at least 50px tall. Back and Next are 36px tall (WCAG 2.5.8). Motion is off with reduced motion.

You must: give each option distinct text, set the right correctAnswer index, and name each quiz when a page has more than one.

Live app mapping

Live (learning-quiz)Code
questions[].correct_answerquestions[].correctAnswer
savedState · onStateChangedefaultState or state · onStateChange (same shape)
saveStatussaveStatus
Option button, results screenQuizOption, QuizResults
No open questions in learning-quiz (lesson practice uses a textarea + “Compare your thinking”; school activities grade short and long text on the server)type: 'open' with evaluate or self-assessment
“QUIZ · 4” section labelPage markup (not part of Quiz)

Guidelines and related components

Use Quiz for

  • Checking understanding at the end of a lesson section
  • Two to six options with one clear right answer
  • Short written answers with a model answer

Don't use Quiz for

  • Settings or preferences (use Radio)
  • Recall practice with no marking (use Flashcard)
  • Questions with several right answers (not supported)

Do and don't

Question 1 of 1

What best defines Human-Centered Design?

DoExplain the answer The explanation turns a mistake into learning.

Question 1 of 1

What best defines Human-Centered Design?

Don'tOnly say right or wrong Learners don't find out why, and may guess again next time.
DoKeep options short and parallel Learners compare options faster when they have the same form.
Don'tHide the answer in the longest option A much longer option gives the answer away.