popizz
v0.1.1
Published
Interactive quiz components for MDX docs — MCQ, true/false, matching, fill-in-the-blank
Maintainers
Readme
popizz
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>Features
- 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
Installation
npm (recommended)
npm install popizz
# or
pnpm add popizz
# or
yarn add popizzThen import the stylesheet once at your app root:
import 'popizz/styles.css'Copy-paste (shadcn-style)
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/stylesCDN
<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>Component API
<Quiz>
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 |
<MCQ> + <Option>
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>
<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>
<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) |
<Match> + <Pair>
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><Explanation>
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>Persistence
By default, reader answers persist to localStorage so progress survives page reloads.
Custom storage
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>Disable persistence
import { Quiz, NoopAdapter } from 'popizz'
<Quiz storage={new NoopAdapter()}>
{/* progress will not be saved */}
</Quiz>Theming
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 →
Dark mode
Auto-detects via prefers-color-scheme: dark. Force a theme by setting [data-theme="light"] or [data-theme="dark"] on a parent element.
Framework Integration
Docusaurus
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,
}Nextra (Next.js)
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
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.
Accessibility
- 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
TypeScript
Fully typed. Import types directly:
import type {
QuizProps, MCQProps, FillInProps, MatchProps,
QuizStorage, QuizState, QuizMode,
} from 'popizz'Browser Support
Modern evergreen browsers. Requires localStorage (gracefully degrades to no persistence in private browsing or sandboxed iframes).
Development
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/Publish Checklist (GitHub + npm)
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.
First-time setup
# 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.
License
MIT
