Skip to Content
📧 Join the Teacharium waitlist to get access. 

Sample Apps

Complete, runnable React apps that host exported lessons with @teacharium/player, each with its own start screen, footer and end screen, saved results, and resume and review modes.

All three samples live in github.com/teacharium/sample-embed-app . They share the same integration and differ in lesson and design, each aimed at a different audience:

SampleAudienceLessonLive demo
Orbit Academy Middle schoolSpace Explorers: a playful learning app with a rocket progress track, stars and badgesOpen
Lumen High schoolAtomic structure: a dark study app with streaks, arrow-key navigation and an accuracy ringOpen
Meridian CollegeSupply and demand: a course portal with an attempts table and a gradebook-style receiptOpen

These samples focus on the host app, not on the lessons. To watch complete lessons being built in Teacharium, see the videos in See it built.

The rest of this page walks through Orbit Academy. Lumen and Meridian follow the same steps with different UI.

Live demo: Open Orbit Academy · Source: samples/orbit-academy 

The demo saves progress in your browser. Use Reset progress in its sidebar to start over.

The sample plays Space Explorers, a seven-question solar system quiz built in Teacharium (the “Example Embedded Lesson”). It uses a different question type on each step: single-answer multiple choice, select-all multiple choice, fill in the blank, ordering, matching, matrix choice, and cloze. The quiz runs inside “Orbit Academy”, a pretend desktop learning app, to show that the player is just a component you place in your own product.

Run it

The repo holds the samples under samples/. This one is samples/orbit-academy; swap in lumen or meridian to run the others:

git clone https://github.com/teacharium/sample-embed-app.git cd sample-embed-app/samples/orbit-academy npm install npm run dev

Open http://localhost:5175 . npm run build produces a static site you can host anywhere. To serve it from a sub-path, build with BASE_PATH, for example BASE_PATH=/demos/orbit/ npm run build. The import map and lesson URL use Vite’s BASE_URL, so they follow the sub-path.

What’s in the box

FeatureFile (in samples/orbit-academy)
Load an export: manifest, widget bundles, lesson.jsonsrc/lib/lessonLoader.ts
Share the app’s React with exported widgetsindex.html, public/react-shim.js, src/main.tsx
Mount the player inside your own framesrc/components/LessonFrame.tsx
Custom footer with Previous and Nextsrc/components/LessonFooter.tsx
Custom start screen with three launch modessrc/components/StartScreen.tsx, src/App.tsx
Custom end screen with per-question resultssrc/components/EndScreen.tsx
Save progress and results to localStoragesrc/lib/progressStore.ts
View the saved data, reset itsrc/components/RawDataPanel.tsx, src/components/ResetDialog.tsx

How it works

1. Serve the export

Export a published version, unzip it, and put its files in public/lesson/:

public/lesson/ manifest.json lesson.json widgets/<widget-id>.js (+ .css) media/... styles/custom.css

To use your own lesson, replace these files. Media references in lesson.json are already rewritten to relative paths such as ./media/<id>.png, so the media resolver only has to prefix the folder:

export const createMediaResolvers = (basePath: string): MediaResolverConfig => ({ useMediaUrl: (ref) => ({ data: ref?.startsWith("./") ? `${basePath}/${ref.slice(2)}` : (ref ?? null), }), useVoiceoverUrl: () => ({ data: null }), // voiceover isn't included in exports });

2. Load the widgets with your React

Widget bundles are ES modules that import from "react" and "react-dom". They register themselves when loaded, and must use the same React instance as the player. The sample does this with an import map and two small shims:

<!-- index.html, before any module script --> <script type="importmap"> { "imports": { "react": "/react-shim.js", "react-dom": "/react-dom-shim.js" } } </script>
// src/main.tsx import React from "react"; import * as ReactDOM from "react-dom"; window.React = React; window.ReactDOM = ReactDOM;

public/react-shim.js re-exports window.React (and react-dom-shim.js re-exports window.ReactDOM). The loader then injects each widget’s <script type="module"> and stylesheet from the manifest and reads lesson.json. After that, getWidgets() from @teacharium/widget returns the registered widgets.

Load widget CSS too. Each widget in manifest.json can have a cssLocalPath. Add it as a <link rel="stylesheet"> or the widgets will render unstyled.

3. Mount the player

<TeachariumPlayer lesson={lesson} widgets={getWidgets()} additionalComponents={{ Text: TextConfig }} mediaResolvers={mediaResolvers} mode={mode} // "player" | "review" initialSessionState={initialSessionState} config={{ allowPreviousStep: true, fontLoading: "google" }} showStartButton={false} renderFooter={(controls) => <LessonFooter controls={controls} />} onStepChange={(state) => progressStore.saveInProgress(lessonId, state)} onComplete={handleComplete} />

The player fills its container, so give the wrapper a height. The sample uses a flex column inside its frame.

4. Custom start screen

showStartButton={false} removes the player’s Start overlay. The lesson starts as soon as the player mounts, so show your own start screen first and only mount the player when the learner picks a mode.

renderFooter replaces the built-in footer. The player calls it with a PlayerFooterControls object and keeps it up to date.

const LessonFooter = ({ controls }: { controls: PlayerFooterControls }) => { const { progress, isLastStep, canGoNext, canGoPrevious, nextButton } = controls; return ( <footer> <button onClick={controls.previous} disabled={!canGoPrevious}> Previous </button> <span> Mission {progress.lessonStepIndex + 1} / {progress.lessonStepCount} </span> <button onClick={controls.next} disabled={nextButton.status === "disabled" || (!isLastStep && !canGoNext)} > {isLastStep ? "Finish" : "Next"} </button> </footer> ); };

On the last step, controls.next() completes the lesson and fires onComplete. Lessons can also drive the footer: a step can disable Next until it’s answered, show a footer Check Answer or Hint button, relabel Next, or show feedback. Those arrive as checkAnswerButton, hintButton, nextButton and feedback, and the sample’s footer draws them when they’re present. In review mode, controls.mode is "review", and Next on the last step calls Done.

6. Save results and progress

The sample keeps one record in localStorage:

interface StoredProgress { version: 1; lessonId: string | null; inProgress: SessionState | null; // the unfinished attempt attempts: Array<{ id: string; completedAt: string; score: number; maxScore: number; items: ItemData[]; // per-question results sessionState: SessionState; // replayed by review mode }>; }
  • onStepChange saves inProgress at every step.
  • Save & exit calls controls.getSessionState(), which also captures answers on the current step, so the learner can leave mid-question.
  • onComplete appends an attempt from the CompletionResult and clears inProgress.

A real app would send the same objects to its backend instead.

7. Three ways to launch

ModeWhat the sample does
Play newClears inProgress and mounts the player with no initialSessionState.
Pick up where you left offMounts the player with initialSessionState={inProgress}. The learner lands on the same step with their answers restored.
Review your previousMounts the player with mode="review" and initialSessionState={latestAttempt.sessionState}. Interactions are disabled, and Previous/Next walk the learner’s recorded path.

Give the player a new key for each session so it remounts cleanly.

8. Custom end screen

onComplete receives score, maxScore, items and the final sessionState. Each item has a stepId, so the sample groups items by step to list each question as correct, not quite, or skipped. A matrix question reports one item per row. It then shows stars, a badge, and buttons to review or play again.

9. View and reset saved data

The sidebar’s View saved data shows the stored JSON with copy and download buttons. Reset progress asks for confirmation, then deletes the record.

Styling

The app’s own UI is plain CSS. The lesson’s look comes from the lesson itself: its custom CSS (included in lesson.json and injected by the player) and its fonts (loaded with fontLoading: "google"). To theme the player’s built-in parts, set the --tp-* CSS variables. Colors take bare HSL channels, for example --tp-primary: 262 83% 58%;.