Ten Sudokus
One puzzle, one Rust API, ten implementations across thirteen repositories. Eight are hand-built framework ports sharing a single component tree — and one of those eight is the original every other repo descends from. Two are single-file apps written entirely by Claude Code. This is what actually separates them.
Nine implementations fill one 3×3 box exactly. The tenth sits outside it, because it came first: the Stencil build predates every other repo and every one of them carries its component names — alert, cell, keys, tool-bar, tw-label.
The two in red are the new ones, styled the way Cell styles a selected square in the apps themselves, on the clrs.cc palette those apps actually ship.
Not shown: sudoku-p4t-wc and sudoku-s4e-wc, custom-element variants of the Preact and Svelte builds. They are the closest cousins to sudoku-cc and are covered below.
01What all ten have in common
Less separates them than you would expect. Every one of the ten plays the same game against the same backend, and every one of them survives a reload.
- Puzzle source
- Every implementation fetches
https://sudoku-rust-api.vercel.app/api/puzzle— your own Rust API. The response is an 81-characterpuzzlestring and areffield that is base64 of the 81 solution digits. Nobody solves anything;atobdoes the work. - Persistence
- All ten keep game state in
localStorage, and all ten namespace the key by project —proto-sudoku::data,sudoku-r3t::app-data,sudoku-cc:game-state,sudoku-juris:game-state. That is not tidiness: GitHub Pages puts every project on one sharedeswat2.github.ioorigin, and an unqualified key hadsudoku-ccadopting and deleting the Juris app's save. - Fallback
- Every implementation carries a hard-coded puzzle so a dead API never blocks a game.
- Hosting
- Vercel, in all cases. How they get there is one of the sharper differences — see §04.
02Two families, one ancestor
A framework comparison
These are not eight Sudoku apps. They are one Sudoku app expressed eight ways, deliberately held constant so the framework is the only variable.
- The same component tree, file for file: Alert, Alerts, Button, Cell, Eswat2Io, Header, Keys, Spinner, SudokuBoard, ToolBar, TwLabel, plus Alien and Fingerprint icons.
- The same utils layer: bag, labels, store, tw-version, tw.
- The same toolchain: pnpm, Vite, Tailwind with proto-tailwindcss-clrs, PostCSS, ky, bumpp, Prettier, cspell, only-allow.
- The same npm scripts, down to bump, twx, cc and spell.
Only the state layer really moves: @stencil/store, Preact signals, Zustand, solid-js/store, a Svelte writable, Pinia, and nanostores for both Lit and uce.
The Stencil build came first — April 2021, five months before the next one. Its component filenames are the ones every later port kept, which makes it less a sibling than the template.
A no-build experiment
These are not ports of the eight. They rebuild the game from scratch under a constraint the others never had: one file, no build step.
sudoku-cc— a singleSudokuBoard extends HTMLElementin open Shadow DOM. Zero dependencies. Nopackage.json, nonode_modules, nothing to install.sudoku-juris— a port ofsudoku-cconto Juris 0.9.0, pulled from unpkg at runtime. Five components over one reactive store. Also no build.- Written by Claude Code, not by hand. Per the READMEs: roughly 200 iterations for
sudoku-cc, all code generated, no manual edits. - They document themselves.
sudoku_readme,sudoku_architectureandsudoku_specificationin one; a blog post and a Juris-vs-Web-Components essay in the other.
Their point of comparison is each other — native Web Components against a tiny reactive framework — not the eight.
03Where the code actually diverges
Four differences matter more than the framework choice. Each changes how you'd read or edit the code.
| The eight framework ports | sudoku-cc & sudoku-juris | |
|---|---|---|
| Board model | A flat list of 81 cell objects with precomputed geometry — Maps for row, column and box, plus pick, locs, keys, solved. Peer highlighting is a lookup. |
Three parallel 9×9 number[][] arrays — originalPuzzle (immutable clues, gates editability), currentPuzzle (live board), solutionPuzzle (answer key, may be null). 0 means empty. Peer highlighting is recomputed arithmetically across all 81 cells on every selection, and goes further than the ports' — it separates row, column and box, and picks out every cell already holding the selected number. |
| Entry & validation | A Keys component in its own row under the board: select a cell and it lists the values its row, column and box don't already use, plus a delete key when the cell holds one of your entries. Validation is deferred and manual: a Check button sits in the toolbar from the moment a puzzle loads — the condition is list.length === 81 && !solved, the board's cell count rather than how many you have filled — and pressing it erases every wrong entry. Only the win is announced, as an Alert; a failed check says nothing. |
The same contextual keypad, rendered inline beside New Puzzle rather than as its own component — getValidNumbers eliminates peers exactly as computeKeys does. Validation is immediate: each placement is checked against the decoded solution as you make it, and a wrong one reverts itself after a 1s timeout. The same correction the ports apply — both erase what doesn't match — without a button and without the wait. |
| Rendering | Whatever the framework does. Components return markup; the runtime reconciles. The Stencil build also renders into Shadow DOM (shadow: true), which makes it the closest structural relative of sudoku-cc. |
sudoku-cc replaces the entire shadowRoot.innerHTML on render, re-attaches every listener, then mutates cells by flat index (row * 9 + col). Any DOM reference held across a render is stale. sudoku-juris uses immutable grid copies to trigger re-render. |
| Styling | Tailwind through a tw() helper, on the clrs palette. A TwLabel component prints the live Tailwind version in the toolbar. |
CSS inlined in template literals. In sudoku-cc it is duplicated across all three render paths — renderGrid, showLoadingState and showError, each assigning a fresh shadowRoot.innerHTML with its own <style> block — so a style change usually needs making more than once. |
| Accessibility | Inconsistent across the set: a real <label> in Stencil, Preact, React, Solid and Vue; a <span> with role and tabindex in Svelte; plain <div>/<span> in Lit and uce. The keypad is a real <button> in all eight, so the keys can always be tabbed to — but only Svelte can also reach a cell from the keyboard (role="button", tabindex="0" and an actual onkeydown), which makes it the one build with a working path end to end. In the other seven the keypad is reachable and never populates. |
Documented as absent in both repos: no ARIA, no keyboard path, no focus management. Cells and keys alike are plain <div>s — .cell, .option-number, .clear-cell — so nothing in the game takes focus at all. The grid needs a pointing device. |
04Build and deploy
This is the widest gap, and the one with the most day-to-day consequence.
Install with pnpm, build with Vite, then deploy by hand:
cd dist; ln -s ../.vercel; vc --prod
One target. The deployed artifact is a hashed bundle you can't diff against the repo, and shipping requires a working local toolchain.
The odd one out. proto-sudoku-wc builds to an npm package and publishes it — currently 0.1.293. Then a second repo, wc-sudoku, deploys the actual site.
That site is 875 bytes: a <head> pointing at jsDelivr, pinned to the exact version, and a single <proto-sudoku /> in the body. Nothing else.
Shipping a change means publishing to npm, then bumping the version in two URLs and the <title>. Two repos, two registries, one page.
Pushing to main is the deploy. Vercel serves index.html statically; GitHub Pages serves the same file from root, with .nojekyll to keep Jekyll out of it.
Because nothing is built, the deployed bytes are exactly the repo's bytes — so curl -s <url> | md5 against md5 index.html is a valid check for whether production is current.
Two targets means index.html must work unmodified on both; anything origin-specific breaks one of them.
05Measured
Everything below was measured from fresh local builds — not estimated. Nine rows date from 5 September 2026; sudoku-juris was re-measured on the 7th, after its framework upgrade.
Bytes over the wire, gzipped
Gzipped JS + CSS from each dist/, all rebuilt 5 Sep 2026. For the single-file apps this is the whole page. Two entries arrive from two origins and are shown split: sudoku-juris is 6.7 KB of app plus 13.7 KB of framework off unpkg, and wc-sudoku is a 0.4 KB page plus 39.9 KB of component off jsDelivr. The wc-sudoku figure is what the browser actually fetched on a cold load — Stencil lazy-loads, so it was measured from the network panel rather than from the package. The sudoku-juris row was re-measured 7 Sep 2026, when it moved from Juris 0.88.2 to 0.9.0 and switched to the minified build the package ships. 0.88.2 was 163.4 KB raw / 28.4 KB gzipped; 0.9.0's juris.js is 130.1 / 21.6, having split four subsystems this app never used into opt-in files rather than removing them; and the juris.mini.js actually loaded is 54.1 / 13.7. That last swap is worth more than the version bump, and it is easy to miss — juris.js and juris.mini.js are different files, and the repo had been serving the unminified one. Every other row still stands as measured on 5 Sep.
The two that load at runtime
Two of the ten deliver a nearly empty page that pulls its real payload from a public CDN. They arrived at that shape four years apart and for different reasons — and they are the only bars worth splitting open.
| Piece | Origin | Raw | Gzipped |
|---|---|---|---|
| sudoku-juris | |||
App code — the whole index.html |
the repo | 33.1 KB | 6.7 KB |
Juris 0.9.0 runtime — juris.mini.js |
unpkg.com | 54.1 KB | 13.7 KB |
| Total over the wire | two origins | 87.2 KB | 20.5 KB |
| wc-sudoku | |||
The wrapper page — index.html |
the repo | 0.9 KB | 0.4 KB |
| proto-sudoku-wc 0.1.293 — 5 files | cdn.jsdelivr.net | 109.8 KB | 39.9 KB |
| Total over the wire | two origins | 110.6 KB | 40.4 KB |
sudoku-cc, for comparison |
the repo, zero dependencies | 45.9 KB | 9.0 KB |
The Juris result cuts both ways. The app code really is smaller than the Web Components version — 6.7 KB against 9.0 KB gzipped, about 26% less for the same game. It just costs 13.7 KB of framework to get there, so the page a visitor actually downloads is 2.3× the size of sudoku-cc.
wc-sudoku pushes the same trade to its limit: the deployed repo holds almost nothing, and 99% of the payload is the published component. The upside the Juris app doesn't get is that the CDN URL is pinned to an exact version, so what ships is immutable and rollback is a one-line edit.
Narrowed to sudoku-juris's inline <script> alone — excluding <head>, page chrome and the CDN tag — the app is 32.2 KB raw / 6.2 KB gzipped across 846 non-blank lines. Neither CDN tag carries an integrity hash, and either app goes down if its CDN does; the Juris repo lists both as known gaps in its own CLAUDE.md.
| Repo | Stack | State | Files | Lines | Commits | Since |
|---|---|---|---|---|---|---|
| proto-sudoku-wc | Stencil 4 → npm → jsDelivr | @stencil/store | 23 | 1,046 | 2,503 | Apr 2021 |
| sudoku-cc | Web Components | instance fields | 1 | 1,295 | 17 | Jun 2025 |
| sudoku-juris | Juris 0.9.0 (CDN) | Juris store | 1 | 1,020 | 4 | Aug 2025 |
| sudoku-p4t | Preact + Vite | @preact/signals | 24 | 1,014 | 1,481 | Jan 2023 |
| sudoku-r3t | React 19 + Vite | Zustand + persist | 21 | 735 | 236 | Apr 2026 |
| sudoku-sld | Solid + Vite | solid-js/store | 25 | 938 | 1,899 | Sep 2021 |
| sudoku-s4e | Svelte + Vite | writable | 23 | 800 | 2,260 | Dec 2021 |
| sudoku-vue | Vue 3 + Vite | Pinia | 22 | 778 | 229 | Apr 2026 |
| sudoku-lit | Lit 3 + Vite | nanostores | 22 | 942 | 1,268 | Feb 2023 |
| sudoku-uce | uce 1.18 + Vite | nanostores | 22 | 1,058 | 1,265 | Feb 2023 |
Lines are not comparable across the two families and shouldn't be read as one column. For the eight framework builds it counts only component and util source under src/ — excluding CSS, index.html and config. For the two single-file apps it is the entire index.html: markup, CSS and script together. The commit counts tell their own story — years of dependency bumps on one side, a handful of generated commits on the other. proto-sudoku-wc's 2,503 don't include the further 1,218 on wc-sudoku, the deploy repo that carries the 875-byte page.
The custom-element variants land where you'd expect: sudoku-p4t-wc at 29.0 KB gzipped over 24 files, sudoku-s4e-wc at 36.1 KB over 23. Both pay roughly 6 KB for the custom-element wrapper. With those two, and the wc-sudoku deploy shell, the family runs to thirteen repositories.
06What each family is for
A controlled experiment in frameworks
Holding the app, the components, the utils, the toolchain and even the npm scripts constant is the whole value. Change one variable — the reactivity model — and the diff between any two repos is a genuine framework comparison rather than a comparison of two people's taste. That only works because there was a template to hold constant, which is what the Stencil build has been since 2021. The long commit histories are the maintenance cost, made visible.
A controlled experiment in generation
Different variable entirely: not which framework, but what an agent produces under a hard constraint — one file, no build, no dependencies — and then whether a tiny reactive framework improves on the native-Web-Components version of the same thing. The generated architecture docs and the Juris-vs-Web-Components essay are part of the output, not notes about it.
Which is why sudoku-cc shipping the smallest payload of the ten isn't really a win over React — it's the constraint doing its job. The more interesting result is at the other end: sudoku-juris hands its lead straight back to a CDN framework, though far less of one than it looks: on Juris 0.9.0's minified build it ships 2.3× sudoku-cc and beats six of the eight framework apps outright. wc-sudoku reached the same thin-page-plus-CDN shape from the opposite direction four years earlier and lands 19.9 KB heavier. Two answers to where does the code live, arrived at independently, that finish nowhere near each other once both are actually minified.