Engineering ~12 min read

Building a 3D algorithm visualizer in pure HTML

A single 2,000-line HTML file. Three.js, ~350 lines of CSS, vanilla JavaScript. No npm, no bundler, no framework. Hosted on GitHub Pages for $0. The result is faster, smaller, and arguably better than what most teams ship after sprinting through React + Vite for a week. Here's the case for choosing constraints.

The brief

I wanted the homepage of lx8labs.com to feature an interactive 3D algorithm visualizer covering everything from Euclid's GCD (300 BCE) to Tim Sort (2002). Each algorithm needed to show its 3D state — sorting bars, graph nodes, DP grids, trees — alongside a code panel that highlighted the executing line in lock-step. A live variable inspector at the bottom. Filtering by name, era, or creator. Keyboard shortcuts. Step-through mode. The kind of thing you'd usually call visualgo.net-grade.

The default modern answer would be: React + Vite + Tailwind + react-three-fiber + zustand for state + maybe TanStack Query if I felt like it. I picked none of that.

The constraint

One HTML file. CDN scripts only. Hosted on GitHub Pages with zero build step. The kind of thing you can email someone as an attachment and they can open it in 2034 and it still works.

"The website is a build artefact. The build artefact should be the website."

That's not original — it's a paraphrase of every htmx essay ever — but it kept me honest. Whenever I felt the urge to reach for a library, I'd ask: is this saving a line of code, or is it adding 200 KB of node_modules to ship 20 KB of feature?

What I ended up with

1
HTML file
~80 KB
Gzipped
39
Algorithms
0
npm deps

The file lives at /algorithms.html. Open it. Inspect the source. The whole thing — sidebar, 3D canvas, code panel, search, keyboard shortcuts, every sorting algorithm, every graph algorithm, every DP solution — is in one document. The only external resources are Three.js r128 and OrbitControls from cdnjs, plus Google Fonts.

The wins

1. Time-to-first-paint is unbeatable

There's no JavaScript bundle to parse before the page paints. The browser sees HTML, paints it, then asynchronously fetches Three.js. By the time Three.js is ready, the user has already been staring at the sidebar and reading "Bubble Sort · 1956 · Edward Friend" for a hundred milliseconds.

Compare to a Vite-built React app: the browser sees an empty <div id="root">, fetches a 200 KB bundle, parses it, runs React's first render. Then maybe paints something.

2. The "view-source" affordance

Right-click → View Source on /algorithms.html shows the entire app. Every CSS rule, every algorithm, every Three.js scene. If a CS student wants to learn how a 3D Quick Sort visualization works, they can — by reading it. No source maps, no minification, no JSX compiling to React.createElement noise.

This was the unexpected delight. The site teaches twice: once by what it shows, once by what it is.

3. Maintenance cost approaches zero

Six months from now, when I want to add a new algorithm, there's nothing to npm audit fix. No vulnerable dependency to patch. No "the framework deprecated this hook" migration. The file is the file.

4. Forking is trivial

Someone wants to take this and build their own visualizer? Save the HTML, open in their editor, change what they want. No "clone, install, configure" ceremony. The file is the project.

The patterns that made it possible

Data-driven sidebar

Every algorithm is an entry in one INFO object:

const INFO = {
  bubble: {
    name: 'Bubble Sort', cat: 'sort',
    year: 1956, creator: 'Edward Friend',
    time: 'O(n²)', space: 'O(1)', stable: 'Yes',
    desc: 'Adjacent-pair swaps bubble the largest…',
    history: "Knuth famously called it 'the wrong algorithm'…"
  },
  // 38 more
};

The sidebar is rendered from this object. Chronological view sorts by year. By-category view groups by cat. Search filters by name/creator/category in one .filter() call. Adding a new algorithm is two things: add an INFO entry + add a run function. The sidebar updates itself.

Synchronized code highlighting via async/await

The algorithm functions are async. They call hl(line) to highlight a pseudocode line, then await tick() to pause. tick() returns a promise that either resolves on a timer (continuous mode) or on a button click (step mode):

const tick = () => {
  if (stepMode) return new Promise(r => stepResolver = r);
  return sleep(Math.max(8, 900 / Math.pow(speed, 1.6)));
};

async function bubbleSort() {
  for (let i = 0; i < N-1; i++) {
    hl(0); setVars({ i, pass: i+1 });
    for (let j = 0; j < N-1-i; j++) {
      hl(1); cmp++; await tick();
      if (arr[j] > arr[j+1]) {
        hl(3); bswap(j, j+1); swp++;
      }
    }
  }
}

The same code structure works for line-by-line debugging and continuous playback. No state machine, no animation engine — just JavaScript's existing async primitives.

Live variable inspector

Each algorithm calls setVars({ key: value }) at meaningful moments. The function diffs against the current state and updates the right-panel display. Cost: ~15 lines of code. Result: the user watches i, j, arr[mid], dist[v], memo[i] change as the algorithm runs.

Where this approach breaks down

I'm not anti-framework. I'm anti-defaulting to a framework. Reach for the bigger toolkit when:

But for this — a personal site with a few interactive demos — every framework decision would have been overhead. The constraint was the design.

What I'd do differently

The takeaway

Static sites are having a moment again, and the reason is: the browsers got really good. fetch, Promise, IntersectionObserver, CSS Grid, matchMedia, requestAnimationFrame, View Transitions, dialog elements, container queries — all native. Most of what a framework gives you in 2026, you can get by reading MDN for a week.

If you're starting a project that fits in one HTML file, start there. Drop into a framework when the constraints actually break. The site I built in three days would have taken three weeks if I'd reached for Next.js first.

Want to see the result?
Open the Algorithm Lab and inspect the source. The whole 2,000-line file is right there.
Open Algorithm Lab →
Alexei Ferreira
Alexei Ferreira DevOps engineer for 13 years, now also building AI tools and writing physics. Founder of Lx8 Labs. Berlin.