1 month ago I posted about building a browser from scratch — no Chromium, no WebKit, no libraries. Just Node.js, Electron, and a Canvas to draw on.
👉 Missed Part 1? → "I Built a Web Browser from Scratch in 42 Days"
That post covered the first 42 days: raw TCP connections, HTML tokenizer, DOM builder, CSS parser, layout engine, and a working Canvas renderer. Basic text showed up. It was exciting but rough.
Since then — approximately 30 more days of building. Here's what Courage can do now.
🆕 What shipped in Days 43–57
Tabs
The most visually obvious change. Courage now has a working tab system:
-
+button creates a new tab - Each tab maintains its own history, URL, and rendered page
- Clicking a tab switches context; the
×button closes it - Active tab is highlighted
Every tab is fully independent — opening a link in a new tab doesn't share state with others.
Clickable Links (Anchor Navigation)
This one took the most debugging. The challenge: Canvas 2D has no DOM events — I had to implement my own hit-testing.
When you click the canvas, Courage:
- Gets the click coordinates via
canvas.getBoundingClientRect() - Walks the layout boxes looking for an anchor whose bounding rect contains that point
- If found, navigates to the
href
I also added a pointer cursor on hover — mousemove triggers the same hit-test, and if the cursor is over a link, cursor: pointer is set on the canvas element. Small detail, huge feel difference.
External CSS Fetching
Before Day 44, Courage only read <style> blocks inline. Now it fetches external stylesheets — it looks for <link rel="stylesheet"> tags in the DOM, fires a second HTTP request for each one, and merges the rules into the cascade. This is what made real sites start to look like real sites.
Class & ID Selector Matching
Added proper .className and #idName selector support. Before this, only tag selectors (h1, p, a) worked. Now the full selector matching order is:
tag → class → ID → pseudo-class (:link, :visited)
CSS var() Resolution — The Big One
GitHub, Wikipedia, and most modern sites define their entire color scheme using CSS custom properties like --color-fg-default on :root. Without var() support, everything was either black or invisible.
I added:
- A new
computed-styles.jsmodule with agetComputedStyle()function -
:rootselector support so variables defined globally actually register -
parentNodereferences indom-builder.jsso the resolver walks up the tree correctly - Pipeline wiring in
browser.jsto run computed styles after selector matching
The resolver works recursively — var(--x) where --x is itself var(--y) chains correctly.
UA Stylesheet + em Units + Bold/Italic
Three smaller but visible wins:
UA stylesheet: Added browser default styles — h1 is now big and bold, h2 slightly smaller, etc. Before this, every element rendered the same size.
em units: Font sizes expressed as 1.5em or 2em now compute correctly via a headingSizes lookup table relative to the parent's font size.
Bold/italic fix: The Canvas ctx.font string has a strict format — bold 16px serif works, 16px bold serif does not. Fixed a bug where both were silently ignored even when the styles matched correctly.
📸 Current state
Here's how Courage has evolved:
| Day 42 | Day 57 |
|---|---|
| Text renders, basic colors | Tabs, links, h1 large & bold |
| Inline CSS only | External CSS fetched over network |
| No link clicking | Click navigates, hover shows pointer cursor |
| No CSS variables |
var() resolves recursively |
🔭 What's next: Attribute Selectors
The next open issue is attribute selector matching — things like:
[data-color-mode="dark"] { ... }
[rel="stylesheet"] { ... }
[type="text"] { ... }
GitHub's entire dark theme is gated behind [data-color-mode="dark"] on the <html> element. Once this lands, CSS variables on GitHub and Wikipedia will finally resolve to the right values.
After that: image rendering. <img> tags currently just get skipped.
💻 Code
Everything is open source:
👉 github.com/Nitin-kumar-yadav1307/Courage
I post updates here as milestones land. If you're curious about how browsers actually work — the TCP handshake, the tokenizer state machine, the CSS cascade, the layout algorithm — the code is readable and heavily iterative. No magic, no black boxes.
Day 57. Still going.

Top comments (0)