Skip to content

devramx/popizz

Repository files navigation

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 popizz
import { 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 id if you need it

Installation

npm (recommended)

npm install popizz
# or
pnpm add popizz
# or
yarn add popizz

Then 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/quiz
npx popizz init --dir src/components/quiz
npx popizz init --css-only --dir src/styles

CDN

<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 / checkbox ARIA 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:

  1. Update package.json version:
    npm version patch
    # or: npm version minor / npm version major
  2. Run release validation locally:
    npm run release:check
    npm run pack:check
  3. Push commit and tag to GitHub:
    git push origin main --follow-tags
  4. Publish to npm:
    npm publish
  5. 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 main branch (require PR + checks).
  • Require the CI workflow status check before merge.
  • Enable Dependabot for npm updates.

License

MIT

About

MDX Pop quizz framework

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors