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
Maintainers
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.
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-pluginPeer Dependencies
This plugin requires the following peer dependencies:
npm install payload @payloadcms/richtext-lexicalOptional (for SEO fields):
npm install @payloadcms/plugin-seoQuick 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-pluginThe 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?
- 📖 Check the documentation
- 🐛 Found a bug? Open an issue
- 💬 Have questions? Discussions
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
