npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-quiz-plugin

v1.2.0

Published

A comprehensive quiz/test system plugin for Payload CMS with multiple choice questions, timed tests, and results tracking

Downloads

450

Readme

Payload Quiz Plugin

A comprehensive quiz and test system plugin for Payload CMS v3. Create timed quizzes with multiple choice questions, automatic grading, and detailed results.

npm version License: CC BY-NC-SA 4.0

Features

  • Collections: Questions, Tests, and Certificate Types
  • Multiple Choice Support: Single or multiple correct answers per question
  • Timed Tests: Configurable time limits with automatic submission
  • Results Tracking: Detailed results with question-by-question review
  • i18n Support: Built-in English and German translations, easily extendable
  • Customizable: Override collections, add custom fields, and configure features
  • React Components: Ready-to-use Quiz UI components for the frontend
  • SEO Ready: Built-in SEO fields for tests using @payloadcms/plugin-seo

Installation

npm install payload-quiz-plugin
# or
pnpm add payload-quiz-plugin
# or
yarn add payload-quiz-plugin

Peer Dependencies

This plugin requires the following peer dependencies:

npm install payload @payloadcms/richtext-lexical

Optional (for SEO fields):

npm install @payloadcms/plugin-seo

Quick Start

1. Add the Plugin to Payload Config

2. Generate Route Templates (Recommended)

After installing the plugin, use the built-in CLI to generate the necessary frontend route files:

npx payload-quiz-plugin

The CLI will ask you about your project structure and generate:

  • /tests/page.tsx - Tests listing page
  • /tests/[slug]/page.tsx - Test detail page
  • /tests/[slug]/start/page.tsx - Quiz start page
  • /tests/[slug]/start/QuizClient.tsx - Main quiz client component
  • /tests/[slug]/results/page.tsx - Results placeholder page

The generator supports:

  • Route groups like (frontend)
  • Locale routing like [locale]
  • Custom app directory paths

After generation, customize the templates to match your project's styling and components.

1. Add the Plugin to Payload Config

// payload.config.ts
import { buildConfig } from 'payload'
import { quizPlugin } from 'payload-quiz-plugin'

export default buildConfig({
  // ... your config
  plugins: [
    quizPlugin({
      i18n: {
        enabled: true,
        defaultLocale: 'en',
      },
      admin: {
        group: 'Quiz', // Admin panel group name
      },
    }),
  ],
})

3. Create Quiz UI Components (Manual)

// app/tests/[slug]/page.tsx
import { getPayload } from 'payload'
import { QuizClient } from './QuizClient'

export default async function TestPage({ params }) {
  const payload = await getPayload({ config: configPromise })

  const test = await payload.findByID({
    collection: 'tests',
    id: params.slug,
  })

  const questions = await payload.find({
    collection: 'questions',
    where: {
      certificateTypes: {
        contains: test.certificateType,
      },
    },
  })

  return <QuizClient test={test} questions={questions.docs} />
}
// app/tests/[slug]/QuizClient.tsx
'use client'

import {
  QuizProvider,
  useQuiz,
  QuizTimer,
  QuizProgress,
  QuestionCard,
  QuizResults,
} from 'payload-quiz-plugin/client'

export function QuizClient({ test, questions }) {
  return (
    <QuizProvider questions={questions} timeLimit={test.timeLimit}>
      <QuizContent test={test} />
    </QuizProvider>
  )
}

function QuizContent({ test }) {
  const {
    state,
    currentQuestion,
    selectAnswer,
    deselectAnswer,
    nextQuestion,
    prevQuestion,
    finishQuiz,
    calculateResults,
  } = useQuiz()

  // Render your quiz UI using these hooks and components
  return (
    <div>
      <QuizTimer timeRemaining={state.timeRemaining} />
      <QuizProgress
        currentIndex={state.currentQuestionIndex}
        totalQuestions={state.questions.length}
      />
      {currentQuestion && (
        <QuestionCard
          question={currentQuestion}
          selectedChoiceIds={state.answers.get(currentQuestion.id) || []}
          onSelectChoice={selectAnswer}
          onDeselectChoice={deselectAnswer}
        />
      )}
    </div>
  )
}

Configuration

Plugin Options

interface QuizPluginConfig {
  i18n?: {
    /** Enable i18n support (default: true) */
    enabled?: boolean
    /** Default locale (default: 'en') */
    defaultLocale?: string
    /** Custom translations */
    translations?: Record<string, QuizTranslations>
  }

  collections?: {
    /** Override Questions collection config */
    questions?: Partial<CollectionConfig>
    /** Override Tests collection config */
    tests?: Partial<CollectionConfig>
    /** Override CertificateTypes collection config */
    certificateTypes?: Partial<CollectionConfig>
  }

  admin?: {
    /** Admin panel group name (default: 'Quiz') */
    group?: string
  }

  features?: {
    /** Enable certificate types (default: true) */
    certificateTypes?: boolean
    /** Enable tests archive block (default: true) */
    testsArchiveBlock?: boolean
  }
}

Custom Translations

Add your own translations or override existing ones:

quizPlugin({
  i18n: {
    enabled: true,
    translations: {
      fr: {
        tests: {
          title: 'Tests',
          startButton: 'Commencer le test',
          finishButton: 'Terminer',
          // ... more translations
        },
        results: {
          congratulations: 'Félicitations !',
          // ... more translations
        },
      },
    },
  },
})

Collection Overrides

Customize the collections with additional fields or settings:

quizPlugin({
  collections: {
    questions: {
      access: {
        read: () => true,
        create: ({ req }) => req.user?.role === 'admin',
      },
      fields: [
        // Additional fields
        {
          name: 'difficulty',
          type: 'select',
          options: ['easy', 'medium', 'hard'],
        },
      ],
    },
  },
})

Collections

Questions

| Field | Type | Description | |-------|------|-------------| | question | textarea | The question text (localized) | | certificateTypes | relationship | Certificate types this question belongs to | | requiredAnswers | number | Number of correct answers to select (1 for single choice) | | choices | array | Answer choices with text, isCorrect flag, and optional explanation | | explanation | richText | General explanation shown after answering |

Tests

| Field | Type | Description | |-------|------|-------------| | title | text | Test name (localized) | | certificateType | relationship | Certificate type for question filtering | | questionCount | number | Number of questions to include | | timeLimit | number | Time limit in minutes | | passMark | number | Percentage required to pass | | allowGoBack | checkbox | Allow revisiting previous questions | | requireAllAnswered | checkbox | Require all questions before submitting | | description | richText | Test description (localized) | | instructions | richText | Test instructions (localized) | | meta | group | SEO fields (title, description, image) |

Certificate Types

| Field | Type | Description | |-------|------|-------------| | title | text | Full certificate name (localized) | | shortName | text | Abbreviated name (e.g., "PSM-I") | | description | textarea | Brief description | | slug | text | URL-friendly identifier |

Components

Server-Side Imports

// For payload.config.ts and server components
import {
  quizPlugin,
  getQuizPluginConfig,
  createQuestionsCollection,
  createTestsCollection,
  createCertificateTypesCollection,
  TestsArchiveBlock,
  createTestsArchiveBlock,
  createTranslator,
  getTranslations,
} from 'payload-quiz-plugin'

Client-Side Imports

IMPORTANT: All React components with hooks must be imported from payload-quiz-plugin/client to ensure proper "use client" directive handling.

// For client components only - do NOT import these from 'payload-quiz-plugin'
import {
  QuizProvider,
  useQuiz,
  QuizTimer,
  QuizProgress,
  QuestionCard,
  ChoiceOption,
  QuizResults,
  TestCard,
  cn, // Tailwind class merge utility
} from 'payload-quiz-plugin/client'

QuizProvider

Wraps your quiz UI and manages state:

<QuizProvider questions={questions} timeLimit={30}>
  {children}
</QuizProvider>

useQuiz Hook

Access quiz state and actions:

const {
  state,           // Current quiz state
  currentQuestion, // Current question object
  isFirstQuestion, // Boolean
  isLastQuestion,  // Boolean
  answeredCount,   // Number of answered questions
  selectAnswer,    // (choiceId: string) => void
  deselectAnswer,  // (choiceId: string) => void
  nextQuestion,    // () => void
  prevQuestion,    // () => void
  goToQuestion,    // (index: number) => void
  finishQuiz,      // () => void
  calculateResults, // (passMark: number) => QuizResult
} = useQuiz()

TestCard

Display a test card with optional custom media component:

<TestCard
  doc={test}
  className="h-full"
  MediaComponent={({ resource, size }) => (
    <YourMediaComponent resource={resource} size={size} />
  )}
/>

Blocks

TestsArchiveBlock

A Payload block for displaying a grid of tests:

// In your page collection
import { TestsArchiveBlock } from 'payload-quiz-plugin'

export const Pages = {
  slug: 'pages',
  fields: [
    {
      name: 'layout',
      type: 'blocks',
      blocks: [
        TestsArchiveBlock,
        // ... other blocks
      ],
    },
  ],
}

Styling

The plugin ships with a CSS theme file that defines all quiz colors as CSS custom properties with sensible defaults and dark mode support.

Setup

Import the theme in your globals.css:

@import 'payload-quiz-plugin/styles';

That's it. The import provides :root defaults and a .dark / [data-theme="dark"] override block. No Tailwind preset or @theme block is needed.

Host project variables

Components also reference standard shadcn/ui-style variables (--primary, --muted, --border, etc.). Make sure your project defines those.

Overriding colors

Override any variable in your own CSS:

:root {
  --quiz-success: oklch(0.72 0.19 160);
  --quiz-error: hsl(25 95% 53%);
}

For dark mode or multi-theme setups, scope overrides to the matching selector:

[data-theme="dark"] {
  --quiz-success: oklch(0.55 0.15 150);
}

[data-theme="ocean"] {
  --quiz-success: oklch(0.65 0.18 180);
}

CSS variable reference

| Variable | Purpose | |----------|---------| | --quiz-success | Correct answer accent | | --quiz-success-foreground | Text on success background | | --quiz-success-light | Light success background | | --quiz-success-border | Success border color | | --quiz-success-text | Success text color | | --quiz-success-muted | Muted success background | | --quiz-success-muted-border | Muted success border | | --quiz-error | Incorrect answer accent | | --quiz-error-foreground | Text on error background | | --quiz-error-light | Light error background | | --quiz-error-border | Error border color | | --quiz-error-text | Error text color | | --quiz-error-muted | Muted error background | | --quiz-error-muted-border | Muted error border | | --quiz-info | Info badge / explanation accent | | --quiz-info-foreground | Text on info background | | --quiz-info-light | Light info background | | --quiz-info-border | Info border color | | --quiz-info-text | Info text color | | --quiz-warning-light | Timer low-time background | | --quiz-warning-text | Timer low-time text | | --quiz-choice-background | Choice button background | | --quiz-choice-foreground | Choice button text | | --quiz-choice-select | Selected choice accent | | --quiz-choice-border | Choice border color | | --quiz-choice-text | Choice label text | | --quiz-choice-muted | Unselected choice fill | | --quiz-choice-muted-border | Choice indicator border |

TypeScript

Full TypeScript support with exported types:

import type {
  QuizPluginConfig,
  QuizQuestion,
  QuizTest,
  QuizResult,
  QuestionResult,
  QuizChoice,
  TestCardData,
} from 'payload-quiz-plugin'

Author

Alexander Sedeke - @alexandrstudio

Created for Alexandr Studio

License

This project is licensed under CC BY-NC-SA 4.0.

  • Attribution — You must give appropriate credit to Alexandr Studio
  • NonCommercial — You may not use this for commercial purposes
  • ShareAlike — Derivatives must use the same license

Support

Need help?

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.