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:
| Sample | Audience | Lesson | Live demo |
|---|---|---|---|
| Orbit Academy | Middle school | Space Explorers: a playful learning app with a rocket progress track, stars and badges | Open |
| Lumen | High school | Atomic structure: a dark study app with streaks, arrow-key navigation and an accuracy ring | Open |
| Meridian | College | Supply and demand: a course portal with an attempts table and a gradebook-style receipt | Open |
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 devOpen 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
| Feature | File (in samples/orbit-academy) |
|---|---|
Load an export: manifest, widget bundles, lesson.json | src/lib/lessonLoader.ts |
| Share the app’s React with exported widgets | index.html, public/react-shim.js, src/main.tsx |
| Mount the player inside your own frame | src/components/LessonFrame.tsx |
| Custom footer with Previous and Next | src/components/LessonFooter.tsx |
| Custom start screen with three launch modes | src/components/StartScreen.tsx, src/App.tsx |
| Custom end screen with per-question results | src/components/EndScreen.tsx |
Save progress and results to localStorage | src/lib/progressStore.ts |
| View the saved data, reset it | src/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.cssTo 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.
5. Custom footer with Previous and Next
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
}>;
}onStepChangesavesinProgressat every step.- Save & exit calls
controls.getSessionState(), which also captures answers on the current step, so the learner can leave mid-question. onCompleteappends an attempt from theCompletionResultand clearsinProgress.
A real app would send the same objects to its backend instead.
7. Three ways to launch
| Mode | What the sample does |
|---|---|
| Play new | Clears inProgress and mounts the player with no initialSessionState. |
| Pick up where you left off | Mounts the player with initialSessionState={inProgress}. The learner lands on the same step with their answers restored. |
| Review your previous | Mounts 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%;.