Quick Answer: Animated code explainers are short, step-by-step animations that show how a code change alters a system. lantern is a zero-dependency TypeScript library that builds them from scenes, renders each frame as SVG, and makes every step addressable by URL. It runs in browsers, Node, and Cloudflare Workers.

Most code walkthroughs are screenshots with arrows drawn on top. They tell you what changed but not in what order things happened, and they go stale the moment someone pushes a commit. Animated code explainers fix the ordering problem, because each step can reveal one idea at a time. The catch is that most animation tooling drags in a browser runtime, a build chain, and a pile of dependencies you then have to maintain. lantern takes a different route, and it's worth understanding why.

What lantern actually is

lantern is a JavaScript port of the core model behind manim, the Python library known for mathematical animations. It keeps manim's coordinate system and animation logic but drops the Python runtime. The frame is 16 by 9 units, the origin sits at the center, and y points up, the same convention manim uses.

The repository is TypeScript throughout. Node 24 runs the tools and tests as .ts files directly through type stripping, which means the code can only use erasable syntax. The library itself has no runtime dependencies. tsc compiles it to dist/ when you need a published-style build.

Here's the catch: lantern isn't on npm yet. You clone the repo and work from source. For a tool still settling its API, that's a reasonable trade-off, but plan for it if you want to drop it into a CI pipeline next week.

Steps are markers, and state is the real content

A lantern scene is a class with an async construct() method. You mark moments with this.step(), and you animate with this.play(). The part most people miss is this.state. It holds the facts the explanation is about, and lantern records it at every step boundary.

Here's a small example, trimmed from the README:

const st = (this.state = { cache: false });
await this.step('before');
// ... draw handler and database, then animate them in
await this.step('after', { pr: 482 });
st.cache = true;

The pr: 482 argument is metadata attached to the step, and the state change between steps is what a reviewer actually cares about. This is why lantern explainers work for code review. You're not just watching shapes move. You're reading a sequence of state transitions, each one tied to a step you can link to.

A typical authoring loop looks like this:

  1. Write the scene class and register it in scenes/index.ts.
  2. Run npm run dev and open http://localhost:5173/?scene=your-scene. The page reloads on save and keeps your current step.
  3. Run npm run see -- your-scene after to print the frame as text, along with the state diff for that step.
  4. Run npm run shot -- your-scene to generate a contact sheet of the end of every step.
  5. Run npm run verify:motion -- your-scene to catch teleports, pops, cuts, and flicker before you ship.

Step 3 is where you'll spend most of your time. Text output means you can review an explainer in a pull request without opening a browser.

Why seeking replays the scene instead of caching frames

This is the part that surprised me, and it contradicts the usual advice. Most animation systems make seeking fast by caching frames or keyframes. lantern does the opposite. A seek re-runs the scene up to time t with frames skipped, which is the same idea as manim's skip_animations.

That works because the loop is fixed-timestep. Each scene advances by dt = 1/fps, and the endpoints of every animation are fixed when begin() runs. Given the same scene, a seek and a full playback produce byte-identical output. The repository's tests assert this for every scene.

The trade-off is real. Replay costs CPU proportional to how far you seek, so a long explainer will feel slower to jump around in than a cached one. But you get a strong guarantee in return: a link to ?scene=&step= always shows exactly what a full run would show. For code explainers that get shared in review threads and Slack, that determinism matters more than scrubbing speed.

The "frame" in a lantern scene is also not a raster. Each frame is written to a sink. StringSink collects SVG, DomSink patches a live <svg> on requestAnimationFrame time, MotionSink samples boxes for the verifier, and TraceSink records state at step boundaries. Same scene, four outputs, one source of truth.

Text that doesn't depend on installed fonts

Most SVG text is a bet on the reader's machine. The <text> element asks the browser or rasterizer to find a font, and when it doesn't find the one you chose, your layout shifts. Glyph widths change, labels overlap, and a diagram that looked fine on your laptop breaks in a Worker-generated PNG.

lantern avoids this by embedding glyph outlines. Inter and JetBrains Mono are included in the repo, and each glyph becomes its own VMobject, a list of cubic Bézier curves. That gives you exact advances, letter-by-letter Write animations, and Transform between strings that morphs glyph to glyph. Every rasterizer draws the same thing, because there's no font lookup at render time.

The cost is size and a limitation you should know about. Glyphs pair by index, not by shape, so TransformMatchingShapes isn't available. If you rename a function and expect lantern to figure out that the old and new names are the same thing, it won't. You'll need to animate the rename explicitly, and that's arguably more honest for an explainer.

Choosing an output: browser, SVG, text, or trace

Not every consumer wants the same thing. The table below maps each output to the entry point the README exposes and the situation where it fits best.

OutputEntry pointBest for
Live pageapp/player.ts, ?scene=&step= linksInteractive walkthroughs in a docs site
One frame as SVGrenderAt() or GET /frame.svgEmbedding a single step in a PR or doc
One frame as textrenderText(), npm run see, or GET /textReviews, diffs, and agents that can't read images
State at every steptraceScene() or GET /trace.jsonAutomated checks and machine consumers
Every framenpm run render -- <scene>Offline video or frame-level diffing

The README also points to Mermaid input. flowchart(mermaidSource) parses a Mermaid flowchart, lays it out, and returns shapes you can animate. sceneFromDoc(doc) plays a JSON document of nodes, edges, code, and operations. If your team already writes architecture diagrams in Mermaid, that's the lowest-friction way in.

Limits to plan around

Before you commit to lantern for a production explainer pipeline, check these limits. The README lists them under "out of scope, for now," and you should treat them as real constraints:

  • No 3D and no boolean path operations, so you can't build solid shapes by subtracting one outline from another.
  • No LaTeX. If your explainers depend on typeset equations, you'll need a different path.
  • Rigid moves rewrite points. Lantern doesn't use SVG transform attributes, so moving a large group means rewriting every point in it. That's fine for explainers with dozens of objects, and you should measure it before you try a few thousand.
  • Collaboration isn't built in. The trace and doc formats are diffable, so you can review them in git. Nothing syncs them in real time.

These limits don't make lantern a poor choice. They tell you where the scope ends. For a focused job, showing how a pull request changes control flow across steps, a small, deterministic, dependency-free tool is often the right answer.

Frequently Asked Questions

What is a steppable code explainer?

A steppable code explainer is an animation where each step is a named, addressable point in a sequence, and the viewer can move forward or back through them. In lantern, each step is a this.step() marker, and the URL ?scene=&step= jumps straight to it. Each step also records the scene's state, so the explainer shows both the picture and the facts behind it.

How do I run lantern locally?

Clone the repository from GitHub, because it isn't published to npm yet. Run npm install, then npm run dev to start a development server at localhost port 5173. Open ?scene=pyex-limits to see an example. Run npm test for the sampled frame checks, which take about 8 seconds, or npm run test:full to check every frame.

Why does lantern replay scenes instead of caching frames?

Replay keeps output deterministic. Because the timestep is fixed and each animation's endpoints are set when it begins, a seek produces the same bytes as a full run. Caching would be faster for long scenes, but it would add a second source of truth that could drift from the scene code. Replay costs CPU when you seek far ahead, and that's the trade-off you're accepting.

When are animated code explainers worth building for code review?

They're worth it when a change affects state over several steps, such as a cache being populated, then invalidated, then read again. A single diff or a static diagram can't show that order clearly. For one-line fixes, a comment is usually faster. Build an explainer when your reviewers keep asking "but what happens first?" and the answer is a sequence rather than a single fact.

Pick one pull request from your backlog that changes behavior over time, not just structure, and turn it into a three-step scene. Use npm run see to read the output as text before you share it, and check whether the state diff matches what the code does. Then send the link to a teammate who keeps asking how a change plays out, and see whether it answers the question without a call.