DEV Community

Nitin Kumar Yadav
Nitin Kumar Yadav

Posted on

Courage Browser — 1 month Later: CSS Variables, Tabs, Link Navigation & More

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

Courage Browser tab system

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:

  1. Gets the click coordinates via canvas.getBoundingClientRect()
  2. Walks the layout boxes looking for an anchor whose bounding rect contains that point
  3. 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.js module with a getComputedStyle() function
  • :root selector support so variables defined globally actually register
  • parentNode references in dom-builder.js so the resolver walks up the tree correctly
  • Pipeline wiring in browser.js to 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"] { ... }
Enter fullscreen mode Exit fullscreen mode

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)