Open source · Windows desktop · Electron

Every surface you live in, side by side — and the dark mode Google fought back on.

MultiView is a split-pane desktop shell: Messages next to Contacts next to Gmail — anything — each pane a real, isolated browser with its own login. Switching workspaces is instant, not a reload. Every site renders dark, including the ones that beat every trick. And it ships as a single portable Windows exe. A personal engineering project; this is its field manual.

Electron 31 / Chromium 126 Render-level force-dark Instant view cache Per-pane userscripts Single-file exe MIT
Messaging drag the seam — left is as Google ships it, right is what MultiView paints
Light Forced dark
Contacts (277)
Name
A
A
A
A
C
Contacts (277)
Name
A
A
A
A
C
the right half is what forceDarkModeEnabled paints — at the compositor, below the DOM

What it is

One window, many isolated browsers, switched without a reload.

Each pane is a real Chromium view — not an iframe, so framing headers can’t block it — grouped into named workspaces you flip between. The whole thing is built around one rule: it has to feel instant while you’re using it.

Shell

Electron BrowserViews — one real isolated Chromium view per site

Sessions

Per-apex partitions — two google.com panes share one login automatically

Workspaces

Named groups — any sites, resizable/collapsible split, edited live

Switching

Persistent view cache — recent workspaces stay warm; flipping back is instant

Dark mode

Global render-level force-dark — with a per-site opt-out for sites that own their theme

Ship

One multiview.exe — electron-builder portable, ~70 MB, runs in place

The build log

Five ways to force dark, one that held.

Google Contacts renders its list inside Shadow DOM and serves it from a service-worker cache — a combination that quietly defeats every approach working through the page’s markup. In order, with why each one died: the platform fights you, and the fix lives below the DOM.

01

Static CSS invert on load

Inject an invert(1) stylesheet when the DOM is ready.

Died — the single-page app re-rendered its own light styling after hydration and painted straight over the rule.

02

Self-healing JS inverter

A luminance check, a timer, and a MutationObserver to re-apply after every re-render.

Died — still light on the real list. The content lives in Shadow DOM the top-level script never sees.

03

Dark Reader engine

Bundle the actual Dark Reader library and enable it per pane.

Died — still DOM-level: it can’t reliably reach Google’s shadow roots and cached shell. Dependency removed.

04

Chromium force-dark via enable-features

The commonly-cited WebContentsForceDark flag.

Died — a silent no-op in Chromium 126. And the “it works” proof was false: the test page (MDN) has its own dark theme. The switch was never active.

05

Force-dark via blink-settings

The renderer setting, driven directly: forceDarkModeEnabled=true.

Held — verified on a control with zero dark support, then the Google page, then the packaged build. Render-level, so it paints Shadow DOM and cached views identically. One line.

Operational notes

Symptoms that look like bugs but are the environment.

  • “Electron failed to install correctly” — npm’s extract-zip silently corrupts the binary unpack on this machine. Extract the cached zip with PowerShell instead.
  • Build “hangs” ten minutes — Windows Defender scanning the fresh 70 MB exe (output file locked). Not a hang; it clears itself.
  • Google login keeps dropping — force-killing the app truncates Chromium’s cookie write. Always close gracefully so it flushes.
  • Gmail goes black-on-black — global force-dark fighting a site that has its own dark theme. Check “own dark” on that pane to exclude it.

Where it goes

Roadmap.

✓ shipped
Nine slices, one at a time. Global dark mode, per-site CSS + JS userscripts, pane collapse, unread badges, a “+” for new workspaces, a per-pane address bar (go temporarily or pin permanently), instant tab switching, per-site “own dark”, double-click tab rename, and single-instance focus.
◆ now
Command-station reach. A global summon hotkey and a fuzzy command palette — invoke, act, dismiss — so the window is something you call, not something you find.
· next
A notification hub with do-not-disturb and per-site routing, and wall-mode geometry (per-workspace monitor placement, auto-rotate) for a dedicated second screen.

Reference

The exact switches.

dark mode
app.commandLine.appendSwitch('blink-settings', 'forceDarkModeEnabled=true')
own-dark opt-out
preload sets document.documentElement.style.colorScheme = 'dark' before first paint → force-dark backs off
switching
keep-alive view cache · LRU cap 5 · idle discard 10 min · active pane never throttled
sessions
persist:apex-<domain> partitions · cookies keyed to the profile’s Local State
build
CSC_IDENTITY_AUTO_DISCOVERY=false npx electron-builder --win portable → multiview.exe (~70 MB)
runtime
Electron 31 · Chromium 126 · Windows · portable, runs in place, no installer

Behind the scenes

The making-of.

MultiView was built by me with Claude as a pair. The record is only trustworthy if it owns the misses — so they’re here, one click away, for anyone who cares.

What the collaboration got wrong, and banked for those who really care · expand

Four misses and the rule each one became.

the missProof on a page that was already dark

A force-dark switch was declared working because a test page went dark — but the page (MDN) had its own dark theme; the switch did nothing. Rule banked: test against a control that can’t fake the result — a plain white page with no dark mode of its own.

the missCalled it done before it was

Dark mode was handed back as fixed more than once while the logged-in view was still light. Rule banked: verify the actual target, not a convenient proxy — a confident wrong answer costs more than “I don’t know.”

the missThe fix that caused the bug

Injecting color-scheme:dark after load, to exempt a site from force-dark, produced the exact black-on-black it was meant to prevent. Rule banked: some things must be set before first paint — a preload at document-start, not a post-load patch.

caught, ownedTested against the live config

Test runs wrote to the real user config and clobbered a workspace that only existed in the running app’s memory. Rule banked: never test against someone’s live data — copy to a throwaway profile first.