Interactive quiz components for MDX docs — MCQ, true/false, matching, fill-in-the-blank.
Drop-in React components for technical writers and bloggers who want "check your understanding" quizzes inline in their documentation. Zero backend required, progress persists in localStorage, fully accessible.
npm install popizzimport { Quiz, MCQ, Option, Explanation } from 'popizz'
import 'popizz/styles.css'
<Quiz>
<MCQ question="Which hook manages side effects in React?">
<Option>useState</Option>
<Option correct>useEffect</Option>
<Option>useRef</Option>
<Explanation>useEffect runs after render for things like data fetching.</Explanation>
</MCQ>
</Quiz>- Five question types — MCQ (single + multi-select), True/False, Match (drag-and-drop), Fill-in (text + numeric)
- Two grading modes — Instant per-question feedback or batch grading with
mode="batch" - Persistence — Reader progress survives page reloads via a strategy-pattern storage layer
- Fully accessible — Keyboard-navigable, ARIA roles, screen reader announcements
- Dark mode — Auto-detects via
prefers-color-scheme, manual override via[data-theme] - Tiny — ~17 KB gzipped (excluding React peer dep)
- Framework-agnostic MDX — Works with Docusaurus, Nextra, Astro, Next.js, Remix
- Zero config IDs — Quizzes auto-identify from page path; explicit
idif you need it
npm install popizz
# or
pnpm add popizz
# or
yarn add popizzThen import the stylesheet once at your app root:
import 'popizz/styles.css'Don't want a dependency? Copy the source into your project:
npx popizz init
# Copies components into ./components/quiznpx popizz init --dir src/components/quiz
npx popizz init --css-only --dir src/styles<link rel="stylesheet" href="https://unpkg.com/popizz/dist/styles.css" />
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/popizz/dist/popizz.umd.js"></script>
<script>
const { Quiz, MCQ, Option } = Popizz;
// ...
</script>The root container. Every quiz component must be a descendant.
| Prop | Type | Default | Description |
|---|---|---|---|
mode |
"instant" | "batch" |
"instant" |
Grade per-question or all at end |
id |
string |
auto | Override auto-generated quiz ID |
storage |
QuizStorage |
localStorage adapter | Custom persistence layer |
showScore |
boolean |
true |
Show score summary when all questions are graded |
Multiple choice. Single-select by default; pass multiple for checkbox behavior.
<MCQ question="Which are valid block-scoped declarations?" multiple>
<Option correct>const</Option>
<Option correct>let</Option>
<Option>var</Option>
<Option>def</Option>
</MCQ>| Prop | Type | Description |
|---|---|---|
question |
string |
Question text |
multiple |
boolean |
Allow multiple correct answers |
<TrueFalse
question="In JavaScript, '===' checks both value and type equality."
answer={true}
/>| Prop | Type | Description |
|---|---|---|
question |
string |
Statement to evaluate |
answer |
boolean |
The correct answer |
<FillIn question="What HTTP code means 'Not Found'?" answer="404" type="numeric" />
<FillIn question="Capital of France?" answer="Paris" type="text" />
<FillIn question="What is π to 2 decimals?" answer={3.14} type="numeric" tolerance={0.01} />| Prop | Type | Default | Description |
|---|---|---|---|
question |
string |
— | Question text |
answer |
string | number |
— | Correct answer |
type |
"text" | "numeric" |
"text" |
Input type |
caseSensitive |
boolean |
false |
For text answers |
tolerance |
number |
0 |
For numeric answers (e.g., 0.01) |
Drag-and-drop matching. Right-side items shuffle on render. Fully keyboard-accessible: Tab to focus, Space to grab, Arrow keys to move, Space to drop.
<Match question="Match each tool to its category:">
<Pair left="React" right="UI Library" />
<Pair left="Webpack" right="Bundler" />
<Pair left="Jest" right="Test Runner" />
</Match>Conditionally rendered after a question is graded. Place inside any question component.
<MCQ question="What is 2+2?">
<Option correct>4</Option>
<Option>5</Option>
<Explanation>
Basic arithmetic: 2 + 2 = 4. You can also write this as 2 × 2.
</Explanation>
</MCQ>By default, reader answers persist to localStorage so progress survives page reloads.
Implement the QuizStorage interface to plug in a different backend (Supabase, Firebase, your API, etc.):
import { Quiz, type QuizStorage, type QuizState } from 'popizz'
class SupabaseAdapter implements QuizStorage {
constructor(private client: SupabaseClient, private userId: string) {}
async get(quizId: string): Promise<QuizState | null> {
const { data } = await this.client
.from('quiz_progress')
.select('state')
.eq('user_id', this.userId)
.eq('quiz_id', quizId)
.single()
return data?.state ?? null
}
async set(quizId: string, state: QuizState) {
await this.client.from('quiz_progress').upsert({
user_id: this.userId,
quiz_id: quizId,
state,
})
}
async clear(quizId: string) {
await this.client.from('quiz_progress').delete()
.eq('user_id', this.userId).eq('quiz_id', quizId)
}
async clearAll() { /* ... */ }
}
<Quiz storage={new SupabaseAdapter(client, userId)}>
{/* ... */}
</Quiz>import { Quiz, NoopAdapter } from 'popizz'
<Quiz storage={new NoopAdapter()}>
{/* progress will not be saved */}
</Quiz>All visuals are driven by CSS custom properties. Override on :root or .pqz-root:
:root {
--pqz-accent: #ff6b35;
--pqz-correct: #00a86b;
--pqz-radius: 4px;
--pqz-font: 'Inter', sans-serif;
}Full list of CSS variables in styles.css →
Auto-detects via prefers-color-scheme: dark. Force a theme by setting [data-theme="light"] or [data-theme="dark"] on a parent element.
Register components globally so authors don't need to import in every MDX file. Create src/theme/MDXComponents.js:
import MDXComponents from '@theme-original/MDXComponents'
import { Quiz, MCQ, Option, TrueFalse, FillIn, Match, Pair, Explanation } from 'popizz'
import 'popizz/styles.css'
export default {
...MDXComponents,
Quiz, MCQ, Option, TrueFalse, FillIn, Match, Pair, Explanation,
}Edit mdx-components.tsx at your project root:
import type { MDXComponents } from 'mdx/types'
import { Quiz, MCQ, Option, TrueFalse, FillIn, Match, Pair, Explanation } from 'popizz'
import 'popizz/styles.css'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...components, Quiz, MCQ, Option, TrueFalse, FillIn, Match, Pair, Explanation }
}Astro is static-first — make sure to add client:load to <Quiz>:
<Quiz client:load mode="instant">
<MCQ question="...">
<Option correct>...</Option>
</MCQ>
</Quiz>Without client:load, the quiz renders as static HTML with no interactivity.
- All controls reachable via
Tab - MCQ uses native
radio/checkboxARIA roles - Match component supports keyboard reordering (Space to grab, arrow keys to move)
- Result badges announce via
aria-live="polite" - Focus indicators on every interactive element
- Color is never the only signal of correctness — icons accompany every state change
Fully typed. Import types directly:
import type {
QuizProps, MCQProps, FillInProps, MatchProps,
QuizStorage, QuizState, QuizMode,
} from 'popizz'Modern evergreen browsers. Requires localStorage (gracefully degrades to no persistence in private browsing or sandboxed iframes).
git clone https://github.com/your-org/popizz
cd popizz
npm install
npm run dev # tsup watch mode
npm test # run vitest
npm run build # produce dist/Use this flow when publishing a new version:
- Update
package.jsonversion:npm version patch # or: npm version minor / npm version major - Run release validation locally:
npm run release:check npm run pack:check
- Push commit and tag to GitHub:
git push origin main --follow-tags
- Publish to npm:
npm publish
- Create a GitHub Release from the new tag and paste release notes.
# npm auth
npm login
# verify package access and ownership
npm whoami
npm access ls-packages $(npm whoami)Recommended repository settings:
- Protect the
mainbranch (require PR + checks). - Require the
CIworkflow status check before merge. - Enable Dependabot for npm updates.
MIT