2025 · Abandoned
A single-file CSS engine that renders to canvas and falls over on deeply nested selectors
A toy CSS parser and layout engine with no dependencies. It worked until it met a selector four levels deep, and I stopped there.
- Role
- Build
- Stack
- TypeScript, Canvas, Parsing
- Source
- Repository
The goal was a CSS engine in one file with no dependencies. Not a browser engine, just enough to take a stylesheet and a flat list of boxes and paint a rectangle. I got about 80% of the way to something I would call correct and then hit a wall I did not want to climb.
The part that worked
Specificity resolution and the cascade were straightforward once I stopped trying to be clever. Sort by origin, then specificity, then document order. That is the whole rule.
function compare(a: Rule, b: Rule): number {
if (a.origin !== b.origin) return a.origin - b.origin;
if (a.specificity !== b.specificity) return a.specificity - b.specificity;
return a.order - b.order;
}Layout for the subset I supported was also fine: block, inline, and a fixed grid. No
floats, no flex, no min-content. Choosing to exclude those kept the whole thing under
900 lines and still useful for the one thing I wanted it for.
The wall
Descendant combinators with resugaring. .a .b .c .d requires walking the ancestor chain
per node, which is fine, but matching it against a live tree while styles are still being
resolved means every pass can invalidate the previous one. I needed a proper
fixpoint computation or I needed to give up.
I gave up. The honest reason is that I had already learned the thing I wanted to learn about specificity, and the remaining work was engineering rather than discovery.
What is left
The repository works for flat stylesheets and fails loudly on nested ones. That is a reasonable place to leave it, and I would rather document the limit than pretend the tool is more general than it is.