Interactive lessons
An Interactive topic plays a web app that you wrote: a 3D scene, a map with charts, a small mathematical toy. You upload it once as a package (a zip file) and use it in as many topics as you like. Each topic can open the package at its own step, play it inline or as the background of the page, and complete when the learner reaches the end of the step range.
What a package is
Section titled “What a package is”A zip file with, at its root:
index.html, the entry file (the manifest may name another);- the files it loads: scripts, styles, images, fonts, data, models, audio, video;
ulams-interactive.json, the manifest.
{ "id": "gravity", "title": { "en": "Gravity: a guided solar system" }, "version": "1.4.0", "entry": "index.html", "licence": "MIT", "attribution": "© 2026 The authors", "source": { "url": "https://example.com/gravity", "ref": "v1.4.0" }, "locales": ["en"], "defaultLocale": "en", "bridge": 1, "capabilities": { "steps": true, "reducedMotion": true, "score": false, "background": true }, "requires": ["webgl"], "network": [], "steps": [ { "id": "what-is-gravity", "title": { "en": "What is gravity?" }, "text": { "en": "Two bodies, Sun and Earth, with equal and opposite force arrows." }, "poster": "posters/what-is-gravity.webp" } ], "a11y": { "keyboard": "Tab reaches all buttons; the 3D view itself is pointer-only." }}Manifest reference
Section titled “Manifest reference”| Field | Required | Rules |
|---|---|---|
id |
yes | a-z, digits and -, 2 to 40 characters. A new version of a package must keep it |
title |
yes | Text per locale; must include defaultLocale |
version |
yes | Your own version string |
entry |
no | An .html file in the zip; default index.html |
licence |
yes | An SPDX id: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, GPL-3.0-only, GPL-3.0-or-later, CC-BY-4.0, CC-BY-SA-4.0, CC0-1.0 or LicenseRef-Proprietary |
attribution, source |
no | Shown to learners under “About this interactive” |
locales, defaultLocale |
yes | Language codes such as en, pl |
bridge |
yes | 1: the protocol version |
capabilities |
no | steps, reducedMotion, score, background: what the package supports |
requires |
no | ["webgl"]: the lesson shows the poster and the text when the browser has no WebGL |
network |
no | Exact https://host[:port] origins the package may call; see Limits and the CSP |
steps |
yes | 1 to 200 steps: id, title and text per locale, and an optional poster image in the zip |
showcase |
no | The loop of a landing hero: steps (1 to 12 step ids) and an optional poster image in the zip; see As the hero of a landing page |
a11y |
no | Notes on keyboard use and alternatives |
Every step needs a text in every locale: it is the text alternative that learners with a screen
reader, reduced motion or no WebGL read instead of the scene. The upload is refused when one is missing.
The JSON Schema is api/packages/interactive/resources/schemas/ulams-interactive/v1.json.
Talk to the lesson page with the bridge
Section titled “Talk to the lesson page with the bridge”The package learns which step to open, and tells the page which step it is on and when it is done, with
the small @ulams/interactive-bridge library (MIT, no dependencies,
one file under 3 KB). Copy dist/interactive-bridge.js into your package:
import { connect } from "./vendor/interactive-bridge.js";
const bridge = connect({ steps: ["what-is-gravity", "inertia"], capabilities: { steps: true, reducedMotion: true }, onInit(init) { /* init.locale, init.theme, init.reducedMotion, init.startStep, init.chrome */ }, onGoToStep(step) { /* show that step */ },});
bridge.stepChanged("inertia"); // the learner moved to another stepbridge.complete(); // the learner finishedbridge.score(8, 10); // a result, for the on_score rulebridge.resize(document.documentElement.scrollHeight);Without a parent page (you open index.html on its own) every call does nothing, so you can develop the
package in a normal browser tab.
Upload and use a package
Section titled “Upload and use a package”- Open Courses → Interactive (
/courses/interactive) and click Upload package. - Choose the
.zip. The package appears with its manifest, steps and licence. If the manifest listsnetworkorigins, the screen shows them and the upload is accepted only after you tick the confirmation box. - To change the package, open it and Upload new version. Versions are immutable: a new upload never changes a topic that already plays an older version.
- In the course editor add an Interactive topic and pick the package.
Topic settings
Section titled “Topic settings”| Setting | Meaning |
|---|---|
| Package and Version | The package, pinned to a version. New topics pin the current version. Choose Follow latest to play every new upload automatically |
| Start step, End step | The range this topic plays. One package can back many topics, each at its own steps |
| Completion | on_range_end (default): the learner reaches the end step, or the package sends complete. on_complete: only complete. on_score: a score of at least the pass score (a percentage). on_open: opening the topic is enough (for packages that do not use the bridge) |
| Display | Inline: the text above and the frame below at the chosen height. Background: the package fills the page and the text floats over it in a card |
| Text | Your own explanation, in Markdown |
A step outside the range is recorded but never completes the topic. Saving fails when a chosen step does not exist in the played version.
What learners get
Section titled “What learners get”- A text version of every step in the topic’s range, in a disclosure under the frame, and the licence, attribution and source link under “About this interactive”.
- In background mode a stepper (Previous, Next), Explore freely to hide the card and use the scene (a Back to the lesson button or Escape brings it back) and spoken step changes for screen readers.
- With reduced motion turned on in the system, and a package that does not say it supports it, the poster of the step is shown instead of the live frame, with a button to play it anyway.
- When WebGL is missing, or the package does not answer within 10 seconds, the poster and the text.
As the hero of a landing page
Section titled “As the hero of a landing page”The landing pages of the free demo academies (gravity, poland, ulam) can open with the course’s own
interactive. The reference frontend asks the public, throttled endpoint GET /api/interactive/showcase, which
returns the launch data of the first interactive topic of the first public course, with no sign-in and no
progress recorded. The hero is not the lesson player: it is a decorative, self-running loop, with no step text
and no controls. A tenant without such a topic answers 404 and the hero shows a drawn scene instead.
To make a package look good there, add a showcase to the manifest and support init.showcase in the package:
"showcase": { "steps": ["diagonals", "primes"], "poster": "posters/showcase.webp" }stepsis the loop, in order, one step every nine seconds. Pick the steps that look best on their own.posteris the still the page paints first, and the only picture shown under reduced motion. Render it from the package with the same view the hero shows (the demo packages use?ulams-poster&ulams-showcase#<step>). Without ashowcase, the hero loops the first four steps and shows the poster of the first.- With
init.showcasethe package hides its text and controls, takes no focus and moves slowly by itself; the page moves it between the steps withgoToStep.
The hero never delays the page: the still is an image of fixed size, the frame starts only after the page has loaded and the browser is idle, it runs only while it is on screen and the tab is visible, and a package that fails or does not answer leaves the still. The picture is hidden from screen readers and has a text alternative. A small link (“Try it”) goes to the lesson, and a button stops the motion.
Limits and the CSP
Section titled “Limits and the CSP”- The upload goes through the upload guard: at most
UPLOADS_INTERACTIVE_MAX_MB(50 MB by default), zip entry and ratio limits, no path tricks, no symbolic links. - Only these file types are accepted:
html htm js mjs css json map txt md svg png jpg jpeg webp avif gif ico woff woff2 ttf otf mp3 ogg wav mp4 webm glb gltf bin wasm csv tsv geojson topojson xml..php,.phar,.htaccess,.svgzand dotfiles are always refused. - Files are served from the tenant content origin, and each version gets its
own Content-Security-Policy from the API: scripts and styles from the package only, no
eval, no frames, no forms,connect-src 'self'. - A manifest
networklist adds origins toconnect-srconly when the tenant settingulams_interactive.allow_networkis on (it is off by default) (Settings). - The whole topic type can be switched off per tenant with
ulams_interactive.enabled. - Progress and scores reach ulams through the learner’s own session. The package never receives a token.
Create topics from the command line
Section titled “Create topics from the command line”ulams topics create-interactive --lesson 12 --title "Too slow" --file gravity.zip \ --start-step too-slow --end-step too-slow --display background --json--file uploads a new package; --package <id> uses an existing one (add --version to pin another
version, or --follow-latest). A manifest that lists network origins needs --accept-network, the
command-line form of the confirmation box in the admin. The MCP tool topics_create_interactive does the same. See CLI.
Example packages
Section titled “Example packages”The repository ships ready-made packages in demo-content/ (MIT for the code, CC BY 4.0 for course text),
which the demo academies use. They are also the best templates for your own.
| Package | What it shows | Steps |
|---|---|---|
gravity |
A Three.js solar system with a guided tour; built with Vite, so it bundles the bridge from source | 44, English and Polish; a poster per step |
poland |
A map and charts of public statistics, plain JavaScript with no build; loads its data with fetch, so it holds ready back with whenReady |
40, English and Polish; a poster per step; honours reduced motion |
ulam/spiral |
A small inline interactive: whole numbers on a square spiral with the primes marked; plain ES modules with a shared step card, keyboard navigation of a canvas, no animation | 4, English; completes when the last step is visited |
ulam/automaton |
Elementary cellular automata, Conway’s Life and the Schrandt-Ulam growth rule (OEIS A170896) from one cell; a canvas editable with the arrow keys; Play is replaced by Step and Run to the end under reduced motion | 5, English; completes when three different steps were visited |
ulam/lwow-map |
A journey on a map: the shared map engine (Mercator, great-circle legs) drawn into an element, a schematic inset, every stop also a button in a list; completes at the end of the topic’s range | 7, English; the notes of each stop cite the fact sheet |
ulam/scottish-book |
Notebook-style cards from data/problems.json (shown as text, never as markup), a filter, a guess per card that sends score; the topic completes with on_score and a pass percentage |
10, English; nine sourced problem cards, a guess on four of them |
ulam/monte-carlo |
Throw seeded random points to estimate π; a log-log chart with a data table; completes from what the learner does (10,000 throws), not from the step | 4, English |
Build one and use it in a lesson (the posters are rendered with Chromium, so it needs
npx playwright install chromium once):
yarn workspace @ulams/demo-gravity packageulams topics create-interactive --lesson 12 --title "Too slow" \ --file demo-content/gravity/release/gravity-ulams-1.0.0.zip \ --start-step too-slow --end-step too-slow --display background --json
# a package with no build step is zipped as it isnode demo-content/scripts/pack.mjs demo-content/polandulams topics create-interactive --lesson 12 --title "Gas" --file demo-content/poland/release/poland-ulams.zip \ --start-step gas --end-step gas --display background --jsonWhat the gravity package teaches about writing your own: guard every storage call, load fonts and textures
from the package (the CSP allows nothing else), keep the step ids stable, send stepChanged for every step and
never complete when the topic plays only a range, and declare reducedMotion: false honestly so learners who
ask for less motion get the poster.
Licences
Section titled “Licences”Choose the SPDX id that matches your code. The licence travels with the package, is shown to learners and is kept in course exports. Platform code is MIT; the bridge is MIT, so packages under any licence may include it.