Lighthouse logo Rust · browser rendering engine from scratch

Watch markup
become boxes.

Lighthouse is a from-scratch browser rendering engine written in pure Rust with zero dependencies. It runs HTML and CSS through the same pipeline a real browser uses, one inspectable stage at a time: an HTML tokenizer and tree builder, a CSS parser with a real cascade, style resolution with specificity and inheritance, block and inline layout, and paint. Type markup and styles and watch the DOM, the computed styles, and the exact box rectangles fall out, live.

Open the playground View on GitHub
How to use this playground

This page runs the exact same pipeline as the Rust engine, reimplemented in JavaScript for teaching: HTML becomes a DOM tree, CSS becomes a stylesheet, the two are combined into computed styles, and those are laid out into boxes and painted onto the canvas. Everything updates as you type.

The controls

  • HTML editor. Type markup on the left. The parser handles nesting, attributes, void elements like <br>, and implicit closing like a bare <li>.
  • CSS editor. Type rules on the right. Selectors can use a tag, a .class, an #id, or a combination. The cascade uses specificity then source order.
  • DOM tree panel. Shows the parsed tree. Click any node to select it.
  • Computed styles panel. Shows the cascaded and inherited values for the selected node, plus its box model numbers.
  • Rendered boxes canvas. Draws the laid out boxes. Toggle Show box edges to overlay margin, border, padding, and content like browser devtools. The selected node is outlined.

A short guided tour

  1. Look at the DOM tree. Notice how nesting in the HTML editor becomes parent and child nodes.
  2. Click the <h1> node. The Computed styles panel fills in. See how color was inherited while font-size came from a rule.
  3. In the CSS editor, change .card width to 240px. Watch the canvas reflow instantly.
  4. Turn on Show box edges. The orange band is margin, yellow is border, green is padding, blue is content.
  5. Add a rule like p { color: crimson; } and then a stronger one, #lead { color: teal; }, on the lead paragraph. The higher specificity id wins. That is the cascade.

every number above is computed by the in-browser engine on this page, the same pipeline as the Rust CLI. Nothing is sent anywhere.

DOM tree

Computed styles

Rendered boxes

margin border padding content

Every stage is a real, inspectable step

Lighthouse implements the real core of a browser: an HTML tokenizer and tree builder, a CSS parser with a proper cascade, style resolution with specificity and inheritance, a block and inline box model, and a paint stage that emits a display list. There is no networking, no JavaScript engine, and no GPU, just the part that turns markup and styles into positioned boxes.

HTML text   -> [html]   -> DOM tree
CSS text    -> [css]    -> Stylesheet
DOM + CSS   -> [style]  -> Styled tree (computed values)
Styled tree -> [layout] -> Layout tree (absolute rectangles)
Layout tree -> [paint]  -> Display list + raster

Three gates back the hardest stages and run in CI. HTML parse correctness serializes the DOM back to HTML and reparses it, asserting the two trees are equal. CSS cascade and specificity is checked against a reference specificity calculation. Layout invariants are checked over randomly generated documents, where every in flow block child must stay inside its parent content box, siblings must not overlap, and no box may have a negative or non finite dimension.

Real output, not a mock

This is what cargo run -- demo prints for the layout stage: every box with its computed content rectangle, including the anonymous block wrappers the engine inserts around inline runs. The same numbers fall out of the playground above.

$ cargo run -- demo == LAYOUT == block <__root__> content=(x:0.0 y:0.0 w:800.0 h:184.0) block <html> content=(x:0.0 y:0.0 w:800.0 h:184.0) block <body> content=(x:10.0 y:10.0 w:780.0 h:164.0) block <div> content=(x:40.0 y:40.0 w:360.0 h:104.0) block <h1> content=(x:44.0 y:44.0 w:352.0 h:33.6) anon-block content=(x:44.0 y:44.0 w:352.0 h:33.6) inline #text "Lighthouse" content=(x:44.0 y:44.0 w:140.0 h:33.6) block <p> content=(x:46.0 y:87.6 w:348.0 h:19.2) anon-block content=(x:46.0 y:87.6 w:348.0 h:19.2) inline #text "A dependency free rendering engine..." content=(x:46.0 y:87.6 w:528.0 h:19.2) block <p> content=(x:46.0 y:118.8 w:348.0 h:19.2) anon-block content=(x:46.0 y:118.8 w:348.0 h:19.2) inline #text "Built by a" content=(x:46.0 y:118.8 w:80.0 h:19.2) inline <span> content=(x:126.0 y:118.8 w:64.0 h:19.2)

The CLI prints the same thing for the DOM, the computed styles, and an ASCII raster of the paint stage, in that order. Every stage is a plain data structure you can print and assert on.

Where Lighthouse sits

Real engines are the ground truth. Lighthouse does not compete with them on coverage or speed. It competes on being readable and inspectable while still running the real stages.

Servo, Blink, WebKit

Production browser engines. Correct and fast across the whole web platform, and effectively opaque: millions of lines, deep in a rendering stack you cannot single step through in an afternoon.

robinson, "let's build a browser engine"

The classic teaching projects that inspired this space. They show the shape of the pipeline well, and usually stop before wiring every stage into one runnable tool with its own correctness gates.

Lighthouse

The readable middle. Small enough to read in an afternoon, complete enough to run HTML and CSS through a real tokenizer, cascade, style resolution, block and inline layout, and paint, with an intermediate representation you can print at every stage and three correctness gates that run in CI. Zero external dependencies.

The stages that matter

Five pipeline stages, three gates.

Each of these is a real module in src/ with plain data types. Feed one stage's output straight into the next.

html Tokenizer and tree builder

Turns HTML text into a DOM tree: nesting, attributes, void elements, implicit closing of paragraphs and list items, and coalesced text runs. Nesting depth is capped so hostile markup cannot overflow the walks.

css Parser and cascade

Parses selectors and declarations into a stylesheet. Tag, class, id, and compound selectors, with specificity then source order deciding the winner. Non finite numbers are rejected at parse time.

style Style resolution

Matches rules to nodes and produces computed values with real specificity ordering and property inheritance, so color flows down while font-size comes from the winning rule.

layout Block and inline box model

Resolves widths, margins, borders, and padding, stacks block boxes, wraps inline runs into anonymous blocks, and clips every in flow box to its containing block so an oversized box is clamped, not escaped.

paint Display list and raster

Walks the layout tree into a display list of colored rectangles and text, then renders a headless ASCII raster. No GPU, no canvas dependency.

gate Correctness gates

Three gates back the hardest stages: DOM round trip serialize and reparse equality, cascade checked against a reference specificity calculation, and layout invariants fuzzed over random overflowing documents.

Use it

Lighthouse ships as a runnable CLI, a dependency free library with stable data types at every stage, a correctness gate you can run yourself, and a stress harness that fuzzes the full pipeline.

CLI

Render a built in sample or a file, optionally with a companion stylesheet. Prints the DOM, the computed styles, the layout box tree, and an ASCII raster of the paint stage.

Library API

html::parse, css::parse, style::style_tree, layout::layout_tree, and paint::build_display_list. Stop and inspect at any stage. No headless Chrome, no opaque black box.

Correctness gates

Three gates plus per module unit tests, all green under cargo test. The layout gate is reproducible through LIGHTHOUSE_FUZZ_OPS and LIGHTHOUSE_FUZZ_SEED.

Stress harness

The stress binary fuzzes layout invariants across millions of seeds and throws structurally hostile HTML and CSS at the pipeline inside catch_unwind with a watchdog that turns any hang into a reproducible failure.

# render a built in sample and print every stage
cargo run -- demo

# render a file, optionally with a companion stylesheet
cargo run -- page.html
cargo run -- page.html --css sheet.css

# run the correctness gates and unit tests
cargo test
LIGHTHOUSE_FUZZ_OPS=5000 LIGHTHOUSE_FUZZ_SEED=1 cargo test fuzz_layout_invariants_hold