Seven widgets, in math
Interactive math in the Bartosz Ciechanowski register: vanilla JavaScript, inline SVG, CSS scroll-driven animations. Zero framework, zero build step.
1. Matrix multiplication — what each entry actually means
The static version on the math page shows row 1 × column 2 of a 2×2 product. It's fine. But matrix multiplication is the kind of operation where watching the row–column pairing slide across the output is more pedagogically useful than reading about it. Hover any of the four output cells below.
2. EML's obstruction — drag $x$ to watch the construction fail
From §8 of the math page: the obvious EML construction $\mathrm{neg}(x) = (e - x) - e$ agrees with $-x$ when $x < e$, and disagrees on $x \geq e$ because of the junk-value extension of $\ln$. The static plot shows it. The draggable plot makes it tactile.
3. The Khovanskii chain, revealed by scroll
The Khovanskii implication chain ends with the headline non-expressibility theorem. On the math page the four steps appear all at once. Below the same chain animates in as you scroll — pure CSS animation-timeline: view(), no JavaScript. Each step appears as it enters the viewport, paced to the reader.
↓ scroll slowly through this section ↓
If you scrolled fast and the steps appeared all at once, scroll back up and try again slower — or you're on Firefox, which still gates scroll-driven animations behind layout.css.scroll-driven-animations.enabled. The fallback is a non-animated render that still reads correctly.
4. PolyAct — the polynomial-activation expressibility frontier
The freshest result on disk (2026-05-11): a polynomial-activation network of arity-1 operators, each applying a polynomial of degree at most $k$, composed to depth $d$, can represent exactly the polynomials of degree $\leq k^d$. The proof is mathlib's Polynomial.natDegree_comp_le applied at each tree level. Fully constructive — no axioms.
So whether $x^N$ is representable by such a network reduces to a single arithmetic comparison: $N \leq k^d$? The widget below makes the comparison tactile. Move the sliders; watch the representability frontier shift; see when your target polynomial drops in or out of range. The headline formalized theorem — $x^9$ is not representable at depth $3$ with degree-$\leq 2$ activations — pops out at $k=2,\, d=3$ (where $k^d = 8 < 9$).
5. Ryser's formula for the permanent
The permanent of an $n \times n$ matrix is $\mathrm{perm}(X) = \sum_{\sigma \in S_n} \prod_{i=1}^{n} X_{i,\sigma(i)}$ — the determinant-without-the-signs. The naive formula has $n!$ terms; exponential in $n$. Ryser (1963) gave an alternative that has $2^n$ terms and admits a polynomial-size arithmetic circuit: $$\mathrm{perm}(X) \;=\; (-1)^n \sum_{S \subseteq [n]} (-1)^{|S|} \prod_{i=1}^{n} \!\left( \sum_{j \in S} X_{i,j} \right).$$ For a $3 \times 3$ matrix the two formulas have $6$ and $8$ terms respectively; the asymptotic gap opens at $n = 4$ ($24$ vs. $16$) and grows. Edit the matrix entries below — both formulas recompute, and the green chip confirms they agree.
6. NAND-tree from a truth table
Sheffer 1913 said every Boolean function on $n$ inputs is expressible as a tree built from only the NAND operator. The proof goes through disjunctive normal form: enumerate the rows of the truth table where the function outputs true; for each such row build a minterm (an AND of literals matching that row); OR the minterms together; then rewrite every AND, OR, and NOT in terms of NAND.
Click the output column of the truth table below to toggle each row. The widget recomputes the function's identity, builds the DNF, and constructs the explicit NAND-tree — using exactly the constructions notTree, andTree, orTree that Sheffer/Examples/Nand.lean proves are NAND-tree representations of their respective Boolean primitives.
7. Monodromy of $z \mapsto z^{1/n}$
The classical building block of Path A. The function $z \mapsto z^{1/n}$ is multivalued on $\mathbb{C} \setminus \{0\}$: at every nonzero $z$, there are $n$ distinct $n$-th roots. The covering map records this — the universal cover unfolds $\mathbb{C}^*$ into $n$ sheets, and dragging a path around the origin in the base space induces a permutation on the sheets. For $z^{1/n}$, that permutation is the cyclic shift $(\sigma : k \mapsto k+1 \bmod n)$ generated by a single counterclockwise loop. The monodromy group is $\mathbb{Z}/n$ — abelian, hence trivially solvable.
Drag the blue dot around the marked branch point at the origin. The widget tracks the cumulative winding number of your path; the sheet wheel on the right highlights which of the $n$ branches you'd currently be on. Loop once, advance one sheet. Loop $n$ times, return home.
8. Techniques used — and why they were enough
The research report from earlier this evening evaluated nine different stacks. For these three widgets, the answer turned out to be the simplest one: the page already had the entire stack. No new framework, no React, no build step, no install. Just three techniques layered on what was already there.
data-* attributes. The visual highlight (yellow row, green column, orange output) is pure CSS. The dot-product readout is updated by setting innerHTML. Total: ~50 lines of vanilla JS. The pattern transfers to any "highlight a relationship" interaction.
input event recomputes $\mathrm{neg}(x)$ in JavaScript and updates the positions of three SVG elements (vertical line, two dots, gap connector) by setting their cx/cy attributes. The piecewise definition of $\mathrm{neg}(x)$ is two lines: if (x < e) return -x; else return 1 - e;. Total: ~80 lines including the SVG markup. This is the Bartosz Ciechanowski pattern — inline SVG plus targeted attribute updates.
animation-timeline: view() with animation-range: entry 0% entry 60%. Each step has a slideUp keyframe; the browser drives the animation as the element enters the viewport. Josh Comeau's article is the best teaching reference. The fallback for browsers without support is a no-op (the @supports not rule), so the content reads correctly even when the animation doesn't run.
k^d — one line; the rendering is two SVG attribute updates per slider event (the green shaded rectangle's width, the orange target marker's $x$). Verdict text and pin (x_pow_9_not_representable_at_depth_3) update as innerHTML. Total: ~70 lines of vanilla JS. The pattern transfers to any "controllable parameter → visualized frontier" widget — likely the most reused interactive shape on this site.
<input type="number"> entries, computes both the $n!$-permutation sum and the $2^n$-subset sum, renders each as a list of contribution rows, totals them, and verifies the two totals match. Each input fires an input event that recomputes everything in ~120 lines of vanilla JS — no virtual DOM, no reactivity framework, no observer plumbing. The two sums are kept in adjacent panels so the structural contrast (exponential vs. polynomial structure) is visible at every keystroke.
Var / Not / And / Or nodes, then transformed recursively into a NAND-only tree via the three direct constructions notTree / andTree / orTree. The output is rendered as indented monospaced text that mirrors the shape of a Lean Term NandBasis n: each NAND at one level, its two children indented one step. The depth count under the tree matches what Sheffer.Term.depth would compute on the corresponding Lean term. ~160 lines including the tree-builder algebra and the renderer.
|Δθ| < π), divide by 2π, accumulate. The current sheet of the n-th-root covering is just floor(winding) mod n. The sheet arcs are rendered as SVG path elements coloured along an OkLCH hue ring; the active arc lights up via a CSS class swap on every redraw. ~140 lines including SVG layout, pointer plumbing, and the trail-rendering.
What this proves, in the small: the recommended-tools list in the research report is real, but for this specific page — Markdown-pandoc-shaped, single document, no framework — the right answer is to not adopt any of them yet. The existing stack plus three small additions (range inputs, SVG attribute manipulation, CSS scroll timelines) reaches the Ciechanowski register without crossing into Mafs/React/Astro territory.
The day that changes is the day there are five interactive widgets per page across ten pages, and the cost of writing each from scratch outweighs the cost of adopting Mafs. We're not there. Until we are, this is the path.