From WKWebView to CEF: Embedding Chromium in a Tauri App

jonnyasmar1 pts0 comments

From WKWebView to CEF: embedding Chromium in a Tauri app — atrium

← All postsatrium has a pane type that holds a real Chromium browser. Not an iframe. Not a "preview" mode of the main webview. A separate Chromium process, rendering inside a tile of the workspace, sitting next to a Claude Code session or a markdown canvas. You can drag-resize it, point it at a localhost dev server, open DevTools on it, and watch hot reload while an AI agent works in the pane next door.

What it looks like from the outside is unremarkable. Web inside a desktop app. We've been doing that for thirty years.

What it took to actually ship was one engine migration I really did not want to do, and an education in where macOS draws the line between "a thing your CSS controls" and "a thing the OS controls." This is meant to be useful to anyone who has thought, even for a minute, I should put a browser inside my desktop app, and then encountered the surprising amount of platform plumbing standing between them and a working version of that.

The spike that worked, until Google OAuth

The first version of atrium's browser pane was a second WKWebView, embedded through Tauri's unstable multi-webview support. This is the path Tauri makes easy. You construct a tauri::webview::WebviewBuilder, point it at a URL, and a sibling webview shows up next to your main one. From the React side, it's just another pane.

The prototype merged into main in early April. For about two weeks I was happy with it. Then I started running into things the WKWebView path made hard, and one thing it made effectively impossible.

The impossible thing was Google OAuth. Modern Google sign-in detects when it's running inside an embedded browser and refuses to complete the flow, a security policy aimed at credential phishing. The exact heuristics aren't public, but in practice WKWebView gets flagged immediately. Users would click "Sign in with Google," watch the redirect happen, and then see a polite refusal page telling them to use a "secure browser." Gmail wouldn't load. Figma's sign-in failed. Anything that routed through Google Identity Services failed.

There are workarounds people use in other apps. Custom user-agent strings. External browser redirects with a deep-link back. We tried a couple. None of them got us out of WKWebView's detection envelope, and the ones that involved bouncing the user out to Safari broke the workspace experience the browser pane existed to preserve. If you're going to embed a browser at all, the entire point is that the user doesn't have to leave your app.

The hard things were less dramatic but added up. WKWebView's inspector speaks Safari Web Inspector protocol, not Chrome DevTools Protocol, which meant the agent-automation surface (programmatic browser navigate, browser eval, browser snapshot) was harder to wire up cleanly. Click and key synthesis worked but felt fragile. And on macOS, hooking WKWebView's hit testing required swizzling a class whose name has the Wry crate version embedded in it (wry::wkwebview::class::wry_web_view::WryWebView0.54.4), discoverable at runtime via objc_getClassList, breakable on every dependency bump.

By mid-April it was clear WKWebView wasn't going to make it past the Google OAuth wall, and that even if we found a workaround, every adjacent capability we wanted from a "real" browser pane was a fight against the engine. The planning doc for the engine swap landed on April 17. The first CEF swap landed over a weekend. The part that took the next month was making it feel native inside the workspace.

What replaced WKWebView is more code, a heavier runtime, and a much more controllable architecture. CEF (Chromium Embedded Framework) is, to be blunt, a more capable embedded browser. It speaks CDP. It moves page rendering into Chromium renderer subprocesses, so a renderer crash in one pane doesn't take down the main app. It supports passkeys and FedCM. In our Google OAuth and Gmail validation, it did not trip the embedded-browser refusal, because the surface is Chromium rather than WKWebView.

The tradeoff is real. CEF adds about 170MB to the app bundle. Initialization is heavier. There's a separate helper subprocess with its own crash boundary. But for a user-facing browser pane that has to work with the entire real web, those costs are the right ones to pay.

The punchout trick

The hardest thing to explain about embedding a non-web view inside a React app is that there is no good way to do it inside React. React renders to the DOM. The DOM lives inside a single webview. A Chromium browser engine running as a separate process cannot be a DOM node, because the DOM was not designed to host that kind of thing.

What you can do is take advantage of macOS's compositing layer. The Tauri app's main window contains an NSView hierarchy. The React-rendering webview is one NSView in that hierarchy. If you set that webview to not draw its own background, and you put an atrium-owned wrapper view (containing the CEF...

browser wkwebview chromium pane inside webview

Related Articles