lmscn — Component Reference

A collection of plug-and-play React learning components built on shadcn/ui and Tailwind CSS v4. Drop any component into your Next.js project and wire up an onComplete callback to integrate with your LMS backend.


Table of Contents


Quick Start

Install the shadcn/ui primitives and the lmscn components you need:

bash
# Install all required primitives at oncenpx shadcn@latest add button card progress badge input scroll-area separator tooltip popover
# Add components via direct URLnpx shadcn@latest add https://lmscn.vercel.app/r/quiz.json

See the for per-component installation and Tailwind v4 configuration details.


Components

Quiz

A multi-question quiz supporting single-choice, multiple-choice, and true/false question types. Shows per-option explanations after answering, tracks score against a configurable passing threshold, and renders a results screen on completion.

tsx
import { Quiz } from "@/components/lms/quiz"import type { QuizData, QuizResult } from "@/components/lms/quiz"
const data: QuizData = {  title: "JavaScript Basics",  description: "Test your knowledge of JS fundamentals.",  passingScore: 80,  showExplanations: true,  shuffle: false,  questions: [    {      id: "q1",      type: "single",      question: "What does `typeof null` return?",      options: [        { id: "a", label: "null" },        { id: "b", label: "object", explanation: "A historical quirk in the JS spec." },        { id: "c", label: "undefined" },      ],      correctIds: ["b"],      hint: "It's a well-known JavaScript oddity.",    },    {      id: "q2",      type: "multiple",      question: "Which are falsy values in JavaScript?",      options: [        { id: "a", label: "0" },        { id: "b", label: '""' },        { id: "c", label: '"false"' },        { id: "d", label: "null" },      ],      correctIds: ["a", "b", "d"],      points: 2,    },  ],}
export function MyQuiz() {  return (    <Quiz      quizData={data}      onComplete={(result: QuizResult) => {        console.log(`${result.percentage}% — ${result.passed ? "Passed" : "Failed"}`)      }}    />  )}

Props — QuizProps

PropTypeRequiredDescription
quizDataQuizDataQuiz configuration and questions
onComplete(result: QuizResult) => voidCalled when the last question is answered
classNamestringAdditional CSS classes

QuizData

FieldTypeDefaultDescription
titlestringDisplayed in the card header
descriptionstringSubtitle below the title
questionsQuizQuestion[]List of questions
showExplanationsbooleantrueShow per-option explanations after answering
shufflebooleanfalseRandomise question order
passingScorenumber70Minimum percentage (0–100) to pass

QuizQuestion

FieldTypeDescription
idstringUnique identifier
type"single" | "multiple" | "true-false"Selection mode
questionstringThe question text
optionsQuizOption[]Answer options
correctIdsstring[]IDs of the correct option(s)
hintstringOptional hint shown before submitting
pointsnumberPoint value (default: 1)

QuizOption

FieldTypeDescription
idstringUnique identifier
labelstringDisplayed text
explanationstringShown beneath the option after revealing the answer

QuizResult

FieldTypeDescription
scorenumberTotal points earned
maxScorenumberTotal points available
percentagenumberScore as a rounded percentage
passedbooleanWhether percentage >= passingScore
answersRecord<string, string[]>Map of question ID → selected option IDs

Flashcards

A flippable flashcard deck with optional spaced-repetition-style self-rating (Again / Hard / Good / Easy). Supports card tags, images on either face, shuffling, and shows a summary breakdown on completion.

tsx
import { Flashcards } from "@/components/lms/flashcards"import type { FlashcardsData, FlashcardsResult } from "@/components/lms/flashcards"
const data: FlashcardsData = {  title: "Spanish Vocabulary",  description: "Core greetings",  shuffle: true,  showRatings: true,  cards: [    { id: "1", front: "Hola",      back: "Hello",     tag: "Greetings" },    { id: "2", front: "Gracias",   back: "Thank you", tag: "Greetings" },    { id: "3", front: "Por favor", back: "Please",    tag: "Greetings" },  ],}
export function MyFlashcards() {  return (    <Flashcards      flashcardsData={data}      onComplete={(result: FlashcardsResult) => console.log(result.counts)}    />  )}

Props — FlashcardsProps

PropTypeRequiredDescription
flashcardsDataFlashcardsDataDeck configuration
onComplete(result: FlashcardsResult) => voidCalled after the last card is rated
classNamestringAdditional CSS classes

FlashcardsData

FieldTypeDefaultDescription
titlestringDeck title
descriptionstringSubtitle
cardsFlashcard[]The cards in the deck
showRatingsbooleantrueShow Again/Hard/Good/Easy buttons after flipping
shufflebooleanfalseRandomise card order

Flashcard

FieldTypeDescription
idstringUnique identifier
frontstringFront face text
backstringBack face text
frontImagestringURL for an image on the front face
backImagestringURL for an image on the back face
tagstringCategory badge shown on the front

FlashcardsResult

FieldTypeDescription
ratingsFlashcardRating[]Per-card difficulty ratings
countsRecord<FlashcardDifficulty, number>Tally of Again / Hard / Good / Easy

FlashcardDifficulty is "again" | "hard" | "good" | "easy".


Match

A two-column matching exercise. The learner clicks a term on the left, then its definition on the right. Correct pairs lock with a green highlight; incorrect pairs briefly flash red. Tracks mistakes and elapsed time.

tsx
import { Match } from "@/components/lms/match"
export function MyMatch() {  return (    <Match      matchData={{        title: "Capital Cities",        shuffle: true,        pairs: [          { id: "1", left: "France", right: "Paris" },          { id: "2", left: "Japan",  right: "Tokyo" },          { id: "3", left: "Egypt",  right: "Cairo" },        ],      }}      onComplete={(result) => alert(`${result.mistakes} mistakes in ${result.durationMs}ms`)}    />  )}

Props — MatchProps

PropTypeRequiredDescription
matchDataMatchDataPairs configuration
onComplete(result: MatchResult) => voidCalled when all pairs are matched
classNamestringAdditional CSS classes

MatchData

FieldTypeDefaultDescription
titlestringActivity title
descriptionstringSubtitle
pairsMatchPair[]Term/definition pairs
shufflebooleantrueRandomise the right column

MatchPair

FieldTypeDescription
idstringUnique identifier
leftstringTerm (left column)
rightstringDefinition (right column)
leftImagestringOptional image URL for the term
rightImagestringOptional image URL for the definition

MatchResult

FieldTypeDescription
matchedMatchPair[]All pairs, in original order
mistakesnumberCount of incorrect pair attempts
durationMsnumberTime taken in milliseconds

Fill in the Blank

Sentence-based cloze exercises. Blanks are marked with ___ in the sentence string and rendered as inline text inputs. Supports multiple blanks per sentence, accepted alternatives, and shows corrections on wrong answers.

tsx
import { FillBlank } from "@/components/lms/fill-blank"
export function MyFillBlank() {  return (    <FillBlank      fillBlankData={{        title: "Biology",        showCorrection: true,        questions: [          {            id: "q1",            sentence: "The powerhouse of the cell is the ___.",            answers: ["mitochondria"],            alternatives: [["mitochondrion"]],            hint: "It produces ATP.",          },          {            id: "q2",            sentence: "Water is made of ___ and ___.",            answers: ["hydrogen", "oxygen"],          },        ],      }}      onComplete={(result) => console.log(result.percentage + "%")}    />  )}

Props — FillBlankProps

PropTypeRequiredDescription
fillBlankDataFillBlankDataQuestions configuration
onComplete(result: FillBlankResult) => voidCalled after the last question
classNamestringAdditional CSS classes

FillBlankData

FieldTypeDefaultDescription
titlestringActivity title
descriptionstringSubtitle
questionsFillBlankQuestion[]List of cloze questions
showCorrectionbooleantrueShow correct answers on wrong submission

FillBlankQuestion

FieldTypeDescription
idstringUnique identifier
sentencestringSentence with blanks marked as ___
answersstring[]Correct answer for each blank, in order
caseSensitivebooleanCase-sensitive matching (default: false)
alternativesArray<string[]>Per-blank list of accepted alternative answers
hintstringOptional hint shown before submitting

FillBlankResult

FieldTypeDescription
attemptsFillBlankAttempt[]Per-question attempt records
scorenumberNumber of fully correct questions
maxScorenumberTotal number of questions
percentagenumberRounded score percentage

Scramble

Letter-unscrambling activity. A pool of shuffled letter tiles is presented; the learner taps tiles to build the answer. Supports image and text clues. Tiles can be reset and re-ordered.

tsx
import { Scramble } from "@/components/lms/scramble"
export function MyScramble() {  return (    <Scramble      scrambleData={{        title: "Science Terms",        questions: [          { id: "1", answer: "photosynthesis", clue: "How plants make food" },          { id: "2", answer: "mitochondria",   clue: "Powerhouse of the cell", clueImage: "/cell.png" },        ],      }}      onComplete={(result) => console.log(result.percentage + "%")}    />  )}

Props — ScrambleProps

PropTypeRequiredDescription
scrambleDataScrambleDataQuestions configuration
onComplete(result: ScrambleResult) => voidCalled after the last question
classNamestringAdditional CSS classes

ScrambleData

FieldTypeDescription
titlestringActivity title
descriptionstringSubtitle
questionsScrambleQuestion[]List of scramble questions

ScrambleQuestion

FieldTypeDescription
idstringUnique identifier
answerstringThe correct word or phrase (spaces are stripped for tile generation)
cluestringContext text shown above the tiles
clueImagestringURL of an optional image clue

ScrambleResult

FieldTypeDescription
attemptsScrambleAttempt[]Per-question attempt records
scorenumberNumber of correct answers
maxScorenumberTotal number of questions
percentagenumberRounded score percentage

Order

Drag-and-drop (or keyboard ↑↓) sequencing activity. Items are shuffled and the learner arranges them into the correct order. Shows the correct sequence after an incorrect submission.

tsx
import { Order } from "@/components/lms/order"
export function MyOrder() {  return (    <Order      orderData={{        title: "Star Lifecycle",        questions: [          {            id: "q1",            prompt: "Order the stages of a star's life:",            hint: "Starts with a cloud of gas and dust.",            items: [              { id: "1", label: "Nebula",        description: "A cloud of gas and dust" },              { id: "2", label: "Protostar" },              { id: "3", label: "Main Sequence" },              { id: "4", label: "Red Giant" },              { id: "5", label: "White Dwarf" },            ],          },        ],      }}      onComplete={(result) => console.log(result.percentage + "%")}    />  )}

Props — OrderProps

PropTypeRequiredDescription
orderDataOrderDataQuestions configuration
onComplete(result: OrderResult) => voidCalled after the last question
classNamestringAdditional CSS classes

OrderData

FieldTypeDescription
titlestringActivity title
descriptionstringSubtitle
questionsOrderQuestion[]List of sequencing questions

OrderQuestion

FieldTypeDescription
idstringUnique identifier
promptstringInstruction shown above the list
itemsOrderItem[]Items listed in the correct order
hintstringOptional hint shown before submitting

OrderItem

FieldTypeDescription
idstringUnique identifier
labelstringPrimary label text
descriptionstringOptional sub-label shown below the main label

OrderResult

FieldTypeDescription
attemptsOrderAttempt[]Per-question attempt records
scorenumberNumber of correctly ordered questions
maxScorenumberTotal number of questions
percentagenumberRounded score percentage

Reading Passage

A three-phase reading comprehension component: the learner first reads the passage, then answers comprehension questions (with the passage optionally visible alongside), and finally sees a results summary. Supports plain text and HTML content.

tsx
import { ReadingPassage } from "@/components/lms/reading-passage"
export function MyReading() {  return (    <ReadingPassage      readingPassageData={{        title: "The Water Cycle",        introduction: "Learn how water moves through Earth's systems.",        readingTimeMinutes: 3,        hidePassageOnQuestions: false,        content: `Solar energy drives evaporation, turning liquid water into vapour.This rises, cools and condenses into clouds, falling as precipitation.`,        questions: [          {            id: "q1",            type: "single",            question: "What drives the water cycle?",            options: [              { id: "a", label: "The Moon" },              { id: "b", label: "Solar energy" },            ],            correctIds: ["b"],            explanation: "The sun provides energy to evaporate water.",          },        ],      }}      onComplete={(result) => console.log(result.percentage + "%")}    />  )}

Props — ReadingPassageProps

PropTypeRequiredDescription
readingPassageDataReadingPassageDataPassage and questions configuration
onComplete(result: ReadingResult) => voidCalled after the last question
classNamestringAdditional CSS classes

ReadingPassageData

FieldTypeDefaultDescription
titlestringPassage title
introductionstringSubtitle / introductory note
contentstringThe reading text
contentIsHtmlbooleanfalseRender content as HTML via dangerouslySetInnerHTML
questionsReadingQuestion[]Comprehension questions
readingTimeMinutesnumberEstimated read time shown as a badge
hidePassageOnQuestionsbooleanfalseHide the passage while answering questions

ReadingQuestion

FieldTypeDescription
idstringUnique identifier
type"single" | "multiple" | "true-false"Selection mode
questionstringThe question text
optionsReadingQuestionOption[]Answer options ({ id, label })
correctIdsstring[]IDs of the correct option(s)
explanationstringShown after the question is answered

ReadingResult

FieldTypeDescription
answersReadingAnswer[]Per-question answer records
scorenumberNumber of correct answers
maxScorenumberTotal number of questions
percentagenumberRounded score percentage

Progress Tracker

A visual Duolingo-style learning path. Renders a zigzag sequence of lesson nodes with status indicators (completed, current, available, locked), XP progress, level, and streak. Hovering a node shows a tooltip with score and XP details.

tsx
import { ProgressTracker } from "@/components/lms/progress-tracker"import type { LessonNode } from "@/components/lms/progress-tracker"
export function MyProgress() {  return (    <ProgressTracker      progressTrackerData={{        learnerName: "Alex",        totalXp: 340,        xpToNextLevel: 500,        level: 4,        streak: 7,        units: [          {            id: "u1",            title: "Biology Unit 1",            lessons: [              { id: "l1", title: "The Cell",        status: "completed", xp: 50,  type: "reading", score: 92 },              { id: "l2", title: "Cell Organelles",  status: "current",   xp: 100, type: "quiz" },              { id: "l3", title: "Match Functions",  status: "locked",    xp: 75,  type: "match" },            ],          },        ],      }}      onLessonSelect={(lesson: LessonNode) => console.log("Opening:", lesson.id)}    />  )}

Props — ProgressTrackerProps

PropTypeRequiredDescription
progressTrackerDataProgressTrackerDataLearner state and curriculum
onLessonSelect(lesson: LessonNode) => voidCalled when an available/current lesson is clicked
classNamestringAdditional CSS classes

ProgressTrackerData

FieldTypeDescription
learnerNamestringDisplayed in the header greeting
totalXpnumberCurrent XP total
xpToNextLevelnumberXP threshold for the next level
levelnumberCurrent level
streaknumberDaily streak in days
unitsLearningUnit[]Curriculum units containing lessons

LessonNode

FieldTypeDescription
idstringUnique identifier
titlestringDisplayed next to the node
statusLessonStatus"completed" | "current" | "available" | "locked"
xpnumberXP earned (completed) or on offer (future)
typeLessonTypeIcon abbreviation shown inside the node
scorenumberScore 0–100 if completed

LessonType options: "quiz" | "flashcards" | "match" | "reading" | "video" | "exercise" | "scramble" | "order" | "hotspot" | "spaced-repetition".


Spaced Repetition

An SM-2 spaced-repetition review session. Cards flip on click; after revealing the answer the learner self-grades on a 4-point scale (Again / Hard / Good / Easy). The component computes the next review interval and ease factor and returns them in onComplete.

tsx
import { SpacedRepetition } from "@/components/lms/spaced-repetition"
export function MySR() {  return (    <SpacedRepetition      spacedRepetitionData={{        title: "French Review",        totalCards: 120,        dueCards: [          { id: "1", front: "bonjour",   back: "hello",     interval: 3, easeFactor: 2.5 },          { id: "2", front: "merci",     back: "thank you", interval: 7, easeFactor: 2.8 },          { id: "3", front: "au revoir", back: "goodbye",   interval: 1, easeFactor: 2.1 },        ],      }}      onComplete={(result) => console.log("Hard cards:", result.hardCardIds)}    />  )}

Props — SpacedRepetitionProps

PropTypeRequiredDescription
spacedRepetitionDataSpacedRepetitionDataDeck and due-card configuration
onComplete(result: SpacedRepetitionResult) => voidCalled after all due cards are graded
classNamestringAdditional CSS classes

SpacedRepetitionData

FieldTypeDescription
titlestringSession title
descriptionstringSubtitle
dueCardsReviewCard[]Cards due for review in this session
totalCardsnumberTotal deck size (shown as context)

ReviewCard

FieldTypeDescription
idstringUnique identifier
frontstringQuestion side
backstringAnswer side
tagsstring[]Category badges shown on the front
nextReviewDatestringISO date string of the scheduled review
intervalnumberCurrent interval in days
easeFactornumberSM-2 ease factor (default: 2.5)

SpacedRepetitionResult

FieldTypeDescription
sessionsReviewSession[]Per-card review records with computed next interval and ease factor
hardCardIdsstring[]IDs of cards graded below 3 (need re-study)

ReviewGrade is 0 | 1 | 2 | 3 | 4 | 5 (SM-2 quality scale). Grades below 3 reset the interval.


Hotspot

Image-labelling activity. Hotspot markers are overlaid on an image at percentage-based coordinates; the learner clicks each marker and types the correct label into a popover input. Supports optional descriptions revealed after checking.

tsx
import { Hotspot } from "@/components/lms/hotspot"
export function MyHotspot() {  return (    <Hotspot      hotspotData={{        title: "Heart Anatomy",        questions: [          {            id: "q1",            imageUrl: "/heart-diagram.png",            imageAlt: "Diagram of the human heart",            prompt: "Label the four chambers:",            caseSensitive: false,            points: [              { id: "p1", x: 30, y: 40, label: "Left Ventricle",  description: "Pumps oxygenated blood to the body." },              { id: "p2", x: 60, y: 40, label: "Right Ventricle", description: "Pumps blood to the lungs." },              { id: "p3", x: 30, y: 20, label: "Left Atrium" },              { id: "p4", x: 60, y: 20, label: "Right Atrium" },            ],          },        ],      }}      onComplete={(result) => console.log(result.percentage + "% correct")}    />  )}

Props — HotspotProps

PropTypeRequiredDescription
hotspotDataHotspotDataQuestions and image configuration
onComplete(result: HotspotResult) => voidCalled after the last question
classNamestringAdditional CSS classes

HotspotData

FieldTypeDescription
titlestringActivity title
descriptionstringSubtitle
questionsHotspotQuestion[]List of hotspot questions

HotspotQuestion

FieldTypeDescription
idstringUnique identifier
imageUrlstringURL of the background image
imageAltstringAlt text for the image
promptstringInstruction shown above the image
pointsHotspotPoint[]Marker definitions
caseSensitivebooleanCase-sensitive label matching (default: false)

HotspotPoint

FieldTypeDescription
idstringUnique identifier
xnumberHorizontal position as a percentage from the left edge (0–100)
ynumberVertical position as a percentage from the top edge (0–100)
labelstringThe correct label the learner must type
descriptionstringOptional detail shown in the popover after a correct answer

HotspotResult

FieldTypeDescription
attemptsHotspotAttempt[]Per-question records with per-point answers and scores
totalScorenumberTotal correct labels across all questions
maxScorenumberTotal labels across all questions
percentagenumberRounded score percentage

TypeScript Types Reference

All types are exported from their respective component files.

ts
// quizimport type { QuizData, QuizQuestion, QuizOption, QuizQuestionType, QuizResult, QuizProps } from "@/components/lms/quiz"
// flashcardsimport type { FlashcardsData, Flashcard, FlashcardDifficulty, FlashcardRating, FlashcardsResult, FlashcardsProps } from "@/components/lms/flashcards"
// matchimport type { MatchData, MatchPair, MatchResult, MatchProps } from "@/components/lms/match"
// fill-blankimport type { FillBlankData, FillBlankQuestion, FillBlankAttempt, FillBlankResult, FillBlankProps } from "@/components/lms/fill-blank"
// scrambleimport type { ScrambleData, ScrambleQuestion, ScrambleAttempt, ScrambleResult, ScrambleProps } from "@/components/lms/scramble"
// orderimport type { OrderData, OrderQuestion, OrderItem, OrderAttempt, OrderResult, OrderProps } from "@/components/lms/order"
// reading-passageimport type { ReadingPassageData, ReadingQuestion, ReadingAnswer, ReadingResult, ReadingPassageProps } from "@/components/lms/reading-passage"
// progress-trackerimport type { ProgressTrackerData, LearningUnit, LessonNode, LessonStatus, LessonType, ProgressTrackerProps } from "@/components/lms/progress-tracker"
// spaced-repetitionimport type { SpacedRepetitionData, ReviewCard, ReviewGrade, ReviewSession, SpacedRepetitionResult, SpacedRepetitionProps } from "@/components/lms/spaced-repetition"
// hotspotimport type { HotspotData, HotspotQuestion, HotspotPoint, HotspotAttempt, HotspotResult, HotspotProps } from "@/components/lms/hotspot"