skip to content
all writeups
3 min read

A browser retro: Tauri v2, WebView2 and popups

Notes from shelving a Tauri v2 + WebView2 browser – what worked, where I was wrong about popups, and why the scope doc was the real deliverable.

taurirustdesktopwebview2

I wanted a Windows-only browser that didn’t ship with a crypto wallet, an AI sidepanel or a rewards programme. I got Phase 1 and most of Phase 2 done – tabs, history, bookmarks, settings, find-in-page and address-bar autocomplete – then I found Helium doing the same thing, and I stopped.

The code sits at joshdevous/aero-browser if you want to poke. The retro is in two parts: what WebView2 gives you for free, and the one architectural call I’d unmake tomorrow.

What WebView2 gives you for free

Embarrassingly much. DevTools, favicon fetching, download handling, zoom and find-in-page – none of those took real effort. WebView2 does the browser-engine part; Tauri is just the shell. That’s the pitch for this stack, and it lands.

Phase 1 (tabs, URL bar, navigation, keyboard shortcuts, find-in-page and context menus) and most of Phase 2 (settings, history, bookmarks and address-bar autocomplete) shipped in a handful of weekends. I had an aero:// URL scheme for internal pages, a bookmarks bar with S2 favicons, and debounced history search with arrow-key navigation in the URL bar. Bookmarks persisted to SQLite. Ctrl+H opened history. It felt like a browser.

The stuff I never got to – ad blocking via adblock-rust, Chrome-profile import, multi-profile sandboxing and extension support – those were the problems I was saving for last. Which is the honest admission: I never hit the walls. I hit the ceiling where I’d have had to design real problems. Then I found Helium, and I put the pencil down.

The architecture I’d unmake

Tauri v2 lets one Window contain multiple Webviews (behind the unstable feature flag). I went with:

src-tauri/src/lib.rs
// Browser UI webview – top strip, hosts the SvelteKit app
let ui_webview = WebviewBuilder::new(
    "browser-ui",
    WebviewUrl::App("index.html".into()),
);
window.add_child(
    ui_webview,
    LogicalPosition::new(0.0, 0.0),
    LogicalSize::new(width, chrome_height),
)?;

// One content webview per tab – fills remaining space
let tab_webview = WebviewBuilder::new(
    "tab-0",
    WebviewUrl::External("https://google.com".parse().unwrap()),
).auto_resize();
window.add_child(tab_webview, ...)?;

A single window with a chrome webview on top (76 pixels, SvelteKit) and a content webview per tab stacked below. You could argue this is clean: the chrome can’t reach into the content, and each tab is a separate renderer anyway. I’d argue it back until I tried to render a dropdown.

The address-bar autocomplete and tab right-click menus want to overflow the chrome rectangle. They’re positioned under the URL bar, floating over the content webview below. They can’t – the chrome webview’s DOM is confined to its 76-pixel strip. You can’t CSS your way out of a webview. I ended up adding IPC commands – show_suggestions_popup, show_context_menu – that spawned more webviews positioned on the fly. It worked. It was also wrong.

The mistake was treating a dropdown as a layout problem. It isn’t. Look at how Chromium actually does URL-bar autocomplete: the suggestions popup is a separate top-level OS window with its own HWND. Same for tab context menus – separate window, drawn by the OS menu system, not by the page. It sits above the browser because it’s a sibling to the browser, not a child of its DOM.

Next time, the chrome-webview and content-webview split stays – I don’t want native chrome, and I can’t put arbitrary pages inside an iframe because real sites refuse to frame. What changes is that every popup becomes its own borderless Tauri window, positioned in screen coordinates, closed on focus loss. No z-order fight, because OS windows have their own compositor plane.

What actually shipped

Not the software. The software was Phase 1 and 2.1–2.3 – useful to me for a few weeks while I was dogfooding, unpolished enough that I wasn’t going to hand it to anyone else.

The two things I’d keep are the reconnaissance – WebView2 + Tauri v2 is further along than I expected, and I now know where its limits start – and the PROJECT.md that shipped with the repo. Five phases, an IPC contract, a DB schema and opinions about what belongs in a browser. That one I’d pick up tomorrow if I came back to it. The code I’d probably rewrite.

The rewrite isn’t happening right now because Helium is close enough that I’d rather use it than maintain my own. If I pick aero back up, the first commit deletes show_suggestions_popup and show_context_menu and replaces them with separate Tauri windows.

Projects

what this writeup is about