DOM ENGINEV0.21.3FIZLET

ANIMATION + GAME BRICKS / FOR THE DOM

Small footprint.
Big performance.

PUBLIC BETA v0.21.3 · a 0.x public beta: every brick carries its status

Fizlet™ is the building blocks for animation and games on the DOM. Each brick is a task we wrote hundreds of times over two decades, now kept by AI and human developers who chase performance at all costs. You bring the logic and style, even from a prompt. The kernel is 3.3 KB gzipped. No runtime dependencies, ever.

SCRIPT TAG · 13.0 KB, EVERY BRICK
<script src="https://fizlet.com/fizlet.js"></script>

A stylised mobile tank game. The board turns and its eight stacked layers lift off one by one, each labelled with its measured size: board grid, characters, movement, collision, rules, effects, input and HUD. The collision layer turns red with a fault, a scan passes over it and patches it, it flashes green, and the stack snaps back into the game. It plays on its own, three times round; the play key pauses and replays it, and scrolling or swiping over it (or the arrow keys, once it has focus) steps through the layers by hand.

7.8 KB gz

LIVE · Battlefield Blitz as a stack of bricks · its code loads only once it nears the screen · The layers story

3.3 KBKERNEL GZIP 26BRICKS 0DEPENDENCIES 1rAF PER PAGE 1MSTEPS · NO GC V0.21.3MIT · OPEN SOURCE

2015 THE ORIGIN · BEFORE AI

Code. Sleep.
Drink. Repeat.

SOUND · OFF UNTIL YOU PRESS IT

Fizlet started long before Fizlet. In 2015, years before AI coding tools, Chris Cooley drew three clickable micro animations by hand: headphones that plug in and play, a cup of coffee that fills, a laptop that opens and types. The Wayback Machine captured them on 23 April 2015.

2015 PROPSA DAY AS A LOOPMODE · CODEDESIGN
34.7 KB gz

LIVE · inspired by the 2015 original, with Fizlet flair · tap the laptop, the plant, the notebook, the lamp · tap the phone for country, rock or pop · drag, flick or double-tap the cup · 2015 / Fizlet switches the look · its code loads only once it nears the screen · The origin scene

2015 · BY HAND

The old-school headache

  • Sprite sheetsEvery frame drawn in Illustrator, laid side by side on one sheet, exactly one frame-width apart. Shift a frame by a pixel and the animation jumps.
  • steps(n)The keyframes slide background-position and steps(n) cuts it frame to frame: the cup fills in steps(11), 11 jumps of 84 px in 0.6 s.
  • AngularA click adds or removes one class (paused, close, drinkMe) and the class swaps the animation. Route transitions written by hand.

About a weekChris's estimate, as an experiment; most of it drawing the frames

2026 · FIZLET

The new one, in minutes

  • Same artThe same frames, timings and clicks, rebuilt on the kernel.
  • Bricks@fzlt/core/fx for the notes, splash, steam and code; @fzlt/core/sound for the thump, clack and slurp; @fzlt/core/hud for the score.
  • SettlesReal buttons, keyboard, still frames under reduced motion, and no frames at all once a round is over.

About six minutesof agent time for the remake, measured on the container clock

  • 1,870 B2015, gzip: the original CSS plus a four-line script
  • 4,025 Bthe remake's JS, gzip, the kernel included
  • 11,289 Bthe arcade's JS, gzip: kernel, fx, sound, hud
  • 0frames a second the arcade asks for once a round is over
WHY FIZLET · IN CHRIS'S WORDS
“There were a lot of animations before Fizlet, and in between. We kept writing the same thing over and over, a little different each time for a slightly different use. So we built Fizlet to make it easier for us. Then we figured other people could get something out of it too, so we open-sourced it.”
— CHRIS COOLEY
“Fizlet makes creativity the only blocker, and I have a lot of that!”
— CHRIS COOLEY

▶ BLAST IT · RECALL IT

Blast it.
Recall it.

Every letter in this little page is a real element. Click it, blast it, then press Recall: the ledger puts back every change the engine made.

your.pageBOOTING11.9 KB gz

Hello, page.

Every letter here is a real element on this page. Knock it around, then press Recall.

SIGN UPNEWv0.2
FRAMES
0
STEPS
0
DROPPED
0
FRAME MS
0.00
WAVES
0/8
PARTICLES
0/420
LEDGER
0
LAYOUT IN STEP
0
LIVE · fizlet + input + fx + sound + debug, running this board

FZ THE NAME

Crack the can.
Hear the fizz.

-let, as in applet: a very small app with one job. Fiz-, from the old soda commercial: a can cracks, the shot zooms in on the bubbles and the drip running down the side.

Motion that looks complex, a tiny footprint, and the page as the stage. The whole story of the name.

8.5 KB gz

LIVE · tap the can · The fizz proof piece

00 HEROES AND DIAGRAMS · THE PAGE PROFILE

Costs nothing
while it's still.

Fizlet is not only for games: it is built just as much for landing-page heroes and interactive diagrams, where the page is the product. The page profile is a lighter build of the same bricks for them.

Settle. Pass settle: 0.5 and systems call engine.busy() in any frame where something moves. Half a second after the last one, the engine sleeps: no requestAnimationFrame at all, zero frames, until wake(), its input or start(). Something due later (an intro delay, an autoplay beat) holds it awake with engine.busy(seconds). fps. fps: 30 caps an ambient loop on the shared clock. slot. Loads the scene only once it nears the viewport and the main thread is idle, and replays the tap that woke it.

PAGE BUILD · kernel + slot + input + fx 7,094 B gzip, measured on this build, against a 6.9 KB budget checked in CI
  • 0 frames once settled, offscreen, in a hidden tab, and under reduced motion when you opt in.
  • Lazy scene. The scene's own code loads only once its slot nears the viewport.
  • No game bricks. No sound, pad, hud or match in the page build (a test fails if one gets in).

01 WHY · THE CONTRACT

Nine promises.
Each one tested.

Most engines assume they own the page. Fizlet is a guest: it runs inside someone else's site, on a phone, next to their code, and it leaves no trace. Every brick and every game built on it keeps these.

  1. 01

    One clock per page

    Every engine on the page shares one requestAnimationFrame. A fixed 60 Hz step with a catch-up cap: a long frame drops time, it never spirals.

  2. 02

    Bounded everything

    Pools and the event ring have a fixed capacity and refuse work when full. Nothing grows during a session.

  3. 03

    Zero steady-state allocation

    One million kernel steps of a busy system cause no young-generation GC. Each brick alone allocates nothing.

  4. 04

    No layout reads in the step

    Read in read, simulate in step, write in render. @fzlt/core/debug counts any layout read made during a step.

  5. 05

    Change-only writes

    The ledger skips writes that change nothing. Writing the same value twice makes zero DOM mutations.

  6. 06

    Pause by default

    Hidden tab, page leaving, root off screen, typing in the host's own fields. Zero CPU while paused.

  7. 07

    Own nothing you can't give back

    Every page change goes through the ledger. destroy() restores it exactly, and a newer host edit always wins.

  8. 08

    Embed-safe

    No globals, no global styles, many engines per page. Every root Fizlet owns is CSS-contained. One AbortSignal removes every listener and observer.

  9. 09

    Budgets in CI

    Kernel at most 4 KB gzip; every brick and the page build have their own budget. Over budget, the build fails.

02 KERNEL + BRICKS

Take only
what you need.

One small kernel and a box of bricks, each its own import with its own budget. Sizes are measured on this build: minified, gzipped, with everything each one pulls in. Each brick has a page with a snippet, its whole API and, for most, a live demo.

In-house / experimental

Used in our games. Not a supported multiplayer service. Build your own room server.

03 ONE FRAME, THREE PHASES

Read. Step.
Render.

Every brick works the DOM the same way: read it cheaply, change only what it owns, leave cleanly. That is the part we stopped rewriting. Every frame runs three phases, in order, for every system.

Geometry is read once in read and cached. The simulation runs in step on plain numbers, at a fixed 60 Hz, with no DOM at all. render writes only what changed, through the ledger. The demo above counts layout reads made during a step with @fzlt/core/debug: the number stays at zero.

  1. READonce a frame

    Cached geometry, input edges latched. The only phase that may measure the page.

  2. STEP × 0–4fixed dt = 1/60 s

    The simulation. No DOM reads, no DOM writes, no allocation. Events go into the ring.

  3. RENDERonce a frame · alpha

    Change-only writes through engine.ledger. Sound and effects read the event ring.

04 A FRAME, NOT A FREE-FOR-ALL

One loop.
Bounded work.

One requestAnimationFrame drives every engine on the page. Pools are filled up front and never grow; when one is full it says no, and the frame stays inside its budget.

A slow frame runs at most four catch-up steps and drops the rest of the time (it shows in stats().dropped). Hidden tab, page leaving, root scrolled away, a visitor typing in a field: the engine pauses and costs nothing. Scroll the demo away and back: it sleeps.

RAF · ONE PER PAGE
ENGINES · SHARE IT
herogamewidget
STEPS · CAP 4, THEN DROP
POOL · 24 SLOTS, NEVER GROWS

A long frame (the wide tick) runs four steps and drops the rest. Engines 2 and 3 cost no extra frames.

05 THE PART WE CARE ABOUT MOST

Recall gives
the page back.

Every change Fizlet makes to the page goes through one ledger. It records the original and what the engine wrote. destroy() puts back everything the engine still owns, newest first.

If the host page changed something after the engine did, the host wins: the ledger stops writing it and never restores over it. A style attribute comes back byte for byte. Try it in the demo: blast the heading, then press Recall. That key runs engine.ledger.restore(), the same call destroy() makes.

engine.ledger · WHAT IT OWNSTESTED IN CHROMIUM
CallOwnsFor example
ledger.text(node, v)a text node's valuethe 0s and 1s a shot leaves in a word
ledger.style(el, prop, v)one inline style propertytransforms, custom properties
ledger.attr(el, name, v)an attribute (null removes)aria-busy, data-hp
ledger.cls(el, name, on)one classsmashed, burning
ledger.add(node)a node you put on the pagea canvas, an overlay, debris
  • Change-only. Writing a value that is already there makes no mutation.
  • Host wins. A newer edit by the page is never overwritten or undone.
  • Exact. After destroy() the DOM matches the snapshot taken before.

06 SPEC SHEET

Hard limits,
in actual numbers.

Limits constrain work; they are not frame-rate promises. Real frame time still depends on the page, the fonts and the device.

  • 1rAF per pageHowever many engines run, one requestAnimationFrame drives them.
  • 60 Hzfixed stepSame results on a 30 Hz phone and a 144 Hz monitor.
  • 4catch-up stepsPast that a frame drops time instead of spiralling.
  • 64events a frameThe default ring. Full means dropped and counted, never grown.
  • 1Msteps, 0 young GCsThe kernel test runs a busy system for a million steps.
  • 0mutations on rewriteWriting the same value twice touches nothing (browser test).
  • 420fx particlesThe default cap, struct of arrays. New ones are refused when full.
  • 3.3 KBkernel gzipOf a 4.0 KB budget, checked on every push.

BRICKS · @fzlt/core/… · gzip

  • debug0.47 KB, 32% of its 1.5 KB budget
  • perf1.97 KB, 100% of its 2.0 KB budget
  • input1.30 KB, 87% of its 1.5 KB budget
  • pad2.02 KB, 98% of its 2.1 KB budget
  • collide1.24 KB, 82% of its 1.5 KB budget
  • grid1.61 KB, 99% of its 1.6 KB budget
  • fx2.73 KB, 91% of its 3.0 KB budget
  • camera1.37 KB, 91% of its 1.5 KB budget
  • sprite1.40 KB, 94% of its 1.5 KB budget
  • sound1.04 KB, 69% of its 1.5 KB budget
  • embed1.99 KB, 80% of its 2.5 KB budget
  • slot1.81 KB, 90% of its 2.0 KB budget
  • presence2.15 KB, 98% of its 2.2 KB budget
  • presence/check4.76 KB, 95% of its 5.0 KB budget
  • hud1.28 KB, 85% of its 1.5 KB budget
  • match2.56 KB, 93% of its 2.8 KB budget
  • rules1.29 KB, 86% of its 1.5 KB budget
  • steer1.12 KB, 90% of its 1.3 KB budget
  • coach1.19 KB, 79% of its 1.5 KB budget
  • timeline2.09 KB, 93% of its 2.3 KB budget
  • touch0.30 KB, 61% of its 0.5 KB budget
  • css-clock5.24 KB, 96% of its 5.4 KB budget
  • layout1.71 KB, 85% of its 2.0 KB budget
  • view-canvas0.91 KB, 76% of its 1.2 KB budget
  • arcade23.58 KB, 100% of its 23.6 KB budget
  • board-page4.01 KB, 99% of its 4.1 KB budget

KERNEL, SPLITS AND BUILDS · gzip

  • kernel3.29 KB, 82% of its 4.0 KB budget
  • arcade (hud omitted)17.77 KB, 100% of its 17.8 KB budget
  • page build (kernel+slot+input+fx)6.93 KB, 100% of its 6.9 KB budget
  • ui (js)2.84 KB, 95% of its 3.0 KB budget
  • ui (css)5.02 KB, 100% of its 5.0 KB budget

Sizes measured on this build of v0.21.3, minified and gzipped (1 KB = 1024 bytes), the same way npm run size checks them in CI. Each tile fills to the share of its module's budget (hover a chip for the number). The one-file script-tag bundle (kernel and every brick but debug) is 13.0 KB.

07 INDEPENDENT PACKAGES · SHARED FOUNDATIONS

Build once.
Ship each.

The engine lives in one repo. Each game takes a fingerprinted snapshot, tests itself and tags its own release. Pages serves static files: no runtime iframe, no remote content service, no client framework.

  1. 01src/the engine
  2. 02npm run checktypes · tests · size
  3. 03CI tags vX.Y.Zfrom package.json
  4. 04vendor/fizletsnapshot + ORIGIN.json
  5. 05Pagesstatic, no server

08 BUILT ON FIZLET

Games that
play on your page.

FIRST GAME ON FIZLET · FREE FOR ALL

Battlefield Blitz

Every tank for itself. Parachute in, grab power tiles, call airstrikes, dodge mines. Most kills in three minutes wins. It drops onto any website with one line: shells knock your headings around, and Escape gives the page back.

DROP-IN · ONE LINE ON ANY PAGE
<script src="https://battlefieldblitz.domvirus.com/blitz.js" defer></script>
  • 1 lineon any page
  • 3 minfree-for-all match
  • Escgives the page back

Built on fizlet · input · collide · fx · sound · embed · rules · presence · ui

THE ARCADE · ON FIZLET

DOM VIRUS

The arcade's approach is “the page is the board”: tiny games that play on the page you are reading. It is one use of Fizlet, and the arcade the bricks were proved out on. Now its games run on them: Null Invaders and Byte Runner at 1.0 and Sherman Bytes at 1.1 on @fzlt/core/arcade, Swarm Protocol 1.4 on the kernel.

SQUAD TACTICS · ON FIZLET SINCE 1.0

DOM Battlefield

Twelve tanks, one radio. The game whose board UI became this design system, on the Fizlet kernel since 1.0; two-network matches come in E12.

09 ROADMAP · E0 TO E16

What's done.
What's left.

Fizlet 0.x is a public beta: every brick carries a status flag, and a 0.x minor release may change a BETA brick's API (the CHANGELOG says how). COMPLETE bricks keep theirs. E0 to E8 are the launch scope; a box is ticked only with a release, a merged change or a test behind it. E9 to E16 come after launch, with the bricks each adds (ROADMAP on the bricks page). The first plan, E0 to E6, is ENGINE.md ↗ in the DOM VIRUS repo.

  1. E0NEARLY DONE

    Baseline and archives

    Freeze the pre-engine games as the before picture, so every port has numbers to beat.

    • Perf harness: every app under a 4× CPU throttle (Playwright)(done)
    • Baseline numbers and commits recorded for all six apps(done)
    • Tag engine-baseline in every repo(to do)
    • Battlefield archive selector (versioned paths)(done)
    • Hub TEST A RELEASE selector for every game version, the 1.0 engine cuts included(done)
  2. E1DONE · 0.1.0

    Kernel

    The scheduler, fixed step, auto-pause, pools, event ring, seeded random, the Recall ledger and stats.

    • One rAF for N engines, fixed step with a catch-up cap(done)
    • Zero allocation over 1M steps, exact DOM restore(done)
    • Kernel under the 4 KB budget(done)
  3. E2DONE · 0.19.0

    The first box of bricks

    Small, separately importable bricks lifted from the games, each with tests, a CI size budget and a generated page. layout, view and actor bricks moved to E10 and E11.

    • input · pad · collide · fx · sound · embed · ui(done)
    • grid · sprite · hud · match · board-page(done)
    • slot and the page profile: settle, fps cap, idle load(done)
    • rules · steer · coach · camera · perf · arcade · timeline(done)
    • A status flag on every brick, with its evidence(done)
  4. E3NEARLY DONE

    Port the games

    Every game on the engine, each as a 1.0 engine cut.

    • Null Invaders → Byte Runner → Sherman Bytes (1.0 each)(done)
    • DOM Battlefield 1.0 · Battlefield Blitz 1.0 · Swarm Protocol 1.4(done)
    • No copied shared/ code left in the games(done)
    • Frame time equal or better than the E0 baseline, measured per game(to do)
  5. E4DONE · 2 TO 4 PLAYERS

    Presence and multiplayer

    A few-KB presence client and a room server: two people play Battlefield Blitz on the live site. Multiplayer: in-house. DIY guide for your own server. Battlefield matches and AI players moved to E12.

    • presence brick + determinism check(done)
    • Room server (in-house): invite gate and hard spend caps(done)
    • Battlefield Blitz two players against our room server(done)
    • DIY guide: build your own room server, the wire protocol as the contract(done)
    • presence: a lobby, a resume that keeps its play pass, a resume after a server restart(done)
    • Battlefield Blitz multiplayer: 1V1, FFA and 2V2, 2 to 4 players, computer backfill(done)
    • DOM Crossfire 0.4.0: squad co-op for 2 to 4 people, lockstep on presence(done)
  6. E5DONE

    Docs for humans and AI

    Build a game or micro-interaction in an afternoon, from docs a person or an agent can follow.

    • One page per brick, generated from the source, with its status(done)
    • Gallery and notes, every number measured on the build(done)
    • llms.txt and a sitemap from the same build(done)
    • Docs and guides: the contract, the loop, Recall, budgets, determinism; release notes for every version(done)
    • An API reference for every brick, generated at build time by the TypeScript compiler, and llms-full.txt(done)
    • Authoring guide and recipes by use case: an action on the page, a shooter, two players(done)
    • Live kernel stats on Under the Hood(done)
  7. E6STARTED

    Performance wins

    Smaller and faster every week: the perf harness in CI, the top cost fixed, the win logged.

    • Perf harness and allocation test in CI on every push(done)
    • A weekly run against the baseline(to do)
    • A public log of every win: PERF-LOG.md, generated from the measured wins(done)
  8. E7NEARLY DONE

    Page interactions

    Heroes, diagrams and micro animations on ordinary pages: lazy, contained, settled when still.

    • slot, the page profile, camera and timeline(done)
    • Gallery: one prompt, one animation, its size on show(done)
    • fizlet.com and the DOM VIRUS home hero on Fizlet(done)
    • The DOM Battlefield landing hero on Fizlet(done)
    • The DOM VIRUS Under the Hood labs on Fizlet(done)
    • The DOM VIRUS cartridge console, detail-page scenes and diagrams on Fizlet(done)
    • Page cost (CLS, tap delay, idle CPU) gated in CI(to do)
  9. E8IN PROGRESS

    The game kit

    The shell every page game repeats, so a game is its rules and its art: HUD and control presets, match flow, coach.

    • arcade: the shell under Null Invaders, Byte Runner and Sherman Bytes(done)
    • HUD presets (card, skirmish bar) and swappable touch pads(done)
    • match, coach and steer bricks(done)
    • touch: no page zoom or pan while a game plays, in every arcade game(done)
    • A released game on the skirmish preset: Sherman Bytes 1.1.0(done)
    • Battlefield Blitz on steer and coach(to do)
    • A released game on @fzlt/core/match(to do)
  10. E9DONE · 0.21.2

    On npm

    npm install @fzlt/core: one package, a subpath per brick, published from CI with provenance.

    • @fzlt/core@0.x on npm from the release workflow, with provenance(done)
    • An API snapshot test for every COMPLETE brick(done)
  11. E10IN PROGRESS

    Diagrams

    Interactive diagrams that settle: graphs laid out on the kernel clock.

    • layout (force layout + Barnes-Hut) and view-canvas bricks, EXPERIMENTAL in 0.21.0(done)
    • An interactive diagram on a marketing page, settled at 0 frames(done)
    • Page cost green on the diagram page, in CI(to do)
  12. E11PLANNED

    Actors

    Units, bots and turrets as data: the parts every board game still writes itself.

    • @fzlt/core/actor (target ≤ 1.2 KB gzip)(to do)
    • @fzlt/core/aim (target ≤ 1.0 KB gzip)(to do)
    • @fzlt/core/view-dom (target ≤ 1.5 KB gzip)(to do)
    • One board game moves its units onto them, and shrinks(to do)
  13. E12PLANNED

    Multiplayer everywhere

    Builds on presence (in-house): authoritative rooms, interpolation, and AI agents that join a room like any player.

    • @fzlt/core/snapshot (target ≤ 1.5 KB gzip)(to do)
    • @fzlt/core/agent (target ≤ 1.0 KB gzip)(to do)
    • A DOM Battlefield match between two networks(to do)
    • An AI agent joins a room(to do)
  14. E13PLANNED

    Animation library

    A recipe used twice becomes data; a recipe used across games becomes a brick.

    • @fzlt/core/scene (target ≤ 2.0 KB gzip)(to do)
  15. E14PLANNED

    Accessible play

    Every game playable by keyboard and gamepad, and heard by screen readers.

    • @fzlt/core/announce (target ≤ 0.6 KB gzip)(to do)
    • Gamepad in @fzlt/core/input(to do)
  16. E15PLANNED

    Music

    Background loops from the same tiny synth, on the kernel clock.

    • @fzlt/core/music (target ≤ 1.2 KB gzip)(to do)
  17. E16PLANNED

    1.0

    Every shipped brick COMPLETE or DEPRECATED, the API frozen, the games on 1.0, and smaller and faster than 0.x by a measured margin.

    • Every brick COMPLETE or DEPRECATED(to do)
    • @fzlt/core@1.0.0(to do)

10 QUICK START

Ten lines
to a blast.

One script tag puts the kernel and every brick on window.fizlet. Click the canvas: that's the hero explosion, pooled, paused when you scroll away.

  1. Script tag. fizlet.js (13.0 KB gzip), one file, no build step.
  2. Modules. Import only the bricks you use from https://fizlet.com/dist/.
  3. npm. npm install @fzlt/core, with the subpath exports.
SCRIPT TAG · index.html
<canvas id="board" width="640" height="320"></canvas>
<script src="https://fizlet.com/fizlet.js"></script>
<script>
  const { createEngine, effects } = fizlet;
  const canvas = document.getElementById("board"), ctx = canvas.getContext("2d");
  const engine = createEngine({ root: canvas }), fx = effects();
  canvas.onclick = (ev) => fx.blast(ev.offsetX * 640 / canvas.clientWidth, ev.offsetY * 320 / canvas.clientHeight);
  engine.add({
    step: (e, dt) => fx.step(dt),
    render: () => (ctx.clearRect(0, 0, 640, 320), fx.drawGround(ctx), fx.draw(ctx)),
  });
  engine.start();
</script>
MODULES · only what you use
<script type="module">
  import { createEngine } from "https://fizlet.com/dist/index.js";
  import { effects } from "https://fizlet.com/dist/fx.js";
  import { controls } from "https://fizlet.com/dist/input.js";
</script>

MIT · NO DEPENDENCIES · V0.21.3

Bring the logic and style.