Native popovers over the web pane
Project to let Seed desktop's popovers, menus and dialogs appear above the integrated browser instead of hiding it. Spiked 2026-10-20; the hybrid reorder on a BaseWindow is the recommended route.

Status: decision under revision after the 2026-10-20 spike (see below). Original decision 2026-10-19: native popovers. Owner: Eric. Decided 2026-10-19 while testing the integrated browser (#1138). Part of the browser work described in Plan: a very secure browser in Seed.

The problem

The website in Seed's browser pane is not part of the app's web page. It is a WebContentsView owned by the main process, composited by the operating system as a native layer above the window that holds the React app. Nothing in the app's DOM can draw over it: z-index orders elements inside one web page, and there are two pages here, stacked as native layers.

So today, whenever the app needs to show something that overlaps the pane (the omnibar dropdown, the tracker shield popover, the page menu, a dialog, a toast), the pane hides itself (the obscured state in pages/web-browser.tsx) and the whole website disappears until the overlay closes. It works, and it is safe, but it feels wrong: the page should stay where it is and the popover should sit on top of it, as it does in a browser.

This is the cost of the security model, and the model stays. The earlier version used the in-page <webview> tag, which layers with the DOM, and the plan removed it on purpose: it needs webviewTag on every window and hands the renderer a privilege a hostile page could reach. The pane being a main-process view is what keeps the renderer free of browser privilege.

Options considered

Option

What it does

Why not

Snapshot while hidden

Capture the pane to an image when an overlay opens, show the image in its place, swap the live view back on close

Cheap and looks right at rest, but the page is frozen while the overlay is open and scroll or video stops

Clip instead of hide

Shrink the view's bounds to the part the overlay does not cover

Only works for rectangular overlays at an edge; a centered dialog or a dropdown in the middle cannot be expressed

Offscreen rendering

Render the page to frames and paint them inside the app's DOM, where anything can layer over it

CPU and GPU cost on every frame, input must be forwarded by hand, native scrolling and selection are lost

Native popovers

Draw overlays in their own top-level window layered above the pane

The most work, and the only option where the page stays live and the overlay behaves like a browser's own

Decision: native popovers. It will feel and work the best. Chrome and Safari do the same thing: their dropdowns, menus and permission bubbles are native widgets above the page, never DOM inside it.

Design sketch

    A transparent, frameless, non-resizable child BrowserWindow per overlay, parented to the app window, hasShadow: false, placed at the overlay's screen rectangle. It moves with the parent and closes with it.

    The child loads the same renderer bundle in an "overlay" mode and renders one React subtree: the popover or dialog content. The parent decides what to show and passes props over IPC; the child sends events back (selection, close, keyboard).

    A React component on the parent side, say NativePopover, wraps the existing popover and dialog primitives so call sites do not change: when the app window has a live web pane under the overlay's rectangle, the component renders into a child window; otherwise it renders inline as today.

    Dismissal: a click outside the overlay reaches the parent (or the pane) normally; the parent closes the child. Escape closes it from either side. Focus moves into the child on open and returns to the opener on close.

    The pane never hides for an overlay that is drawn natively. The obscured path stays for anything not yet converted, so conversion can happen one overlay at a time.

Scope, in order

    The omnibar dropdown and search results, the overlay people hit first.

    The pane's own controls: the tracker shield popover and the page menu.

    Dialogs that open while a page is showing: the clear-browsing-data confirmation, agent consent prompts in the assistant panel footer, download warnings (today these are native message boxes already).

    Toasts and the assistant panel are not in scope: they sit beside the pane, not over it.

Open questions

    Keyboard and screen readers: a child window is a separate accessibility tree. Arrow keys inside a dropdown and announcing the popover need testing on macOS and Windows.

    Windows and Linux transparency: transparent child windows have platform quirks (Linux compositors in particular). Fallback to the hide path where transparency is unavailable.

    Multiple displays and DPI: the overlay rectangle must be translated to screen coordinates with the right scale factor; test a window straddling two displays.

    Animation: popover open and close transitions run inside the child; the window appears before the first frame, so either skip the transition or pre-create the window.

    State: the child has its own React tree and query client. Content that reads app state (recent searches, blocked counts) is passed in as props or fetched over the same tRPC bridge.

    Cost: one window per overlay type can be reused and hidden instead of created per open, if creation latency shows.

Spike, 2026-10-20: the browser behind a transparent app view

Eric's alternative: put the website view below the app's own web contents and leave a transparent rectangle in the app UI where the pane sits, so every popover, dialog and toast is ordinary DOM drawn above the page. A standalone Electron 44.5.1 experiment on macOS answered it.

    Z-order. A BrowserWindow cannot do it: Electron inserts the window's own contents as a sibling before contentView in the native tree, so every child view paints above it whatever index it gets, and transparent backgrounds or transparent: true change nothing. A BaseWindow with two WebContentsViews (website at index 0, app UI at index 1) does it, and addChildView on an existing child reorders it in place.

    Transparency. Works on macOS with an opaque window: the app view gets a fully transparent background colour and a transparent body, paints its chrome opaque, and the page shows through the hole, keeps animating, follows the hole on resize, and a DOM popover and toast draw above it. No window-level transparency, so no shadow, resize or traffic-light side effects. Windows and Linux: the docs and tracker say a transparent view inside an opaque window is a compositor-layer property on every platform, but it is unverified for this setup.

    Input, the hard half. Electron 44 has no per-view mouse pass-through; setIgnoreMouseEvents is window-level. An upstream pull request adding setInteractive to views is open, semver-minor, with no backport. Forwarding events from the app view to the page with sendInputEvent works for hover, cursor, wheel, drag selection, right click and keys, at one frame of latency, but trackpad momentum, gestures, drag and drop and accessibility are not covered, so always-behind would make the main process a permanent input proxy with known gaps.

    Hybrid reorder. Keep the website on top while nothing overlaps it. When an overlay opens, move it below the transparent app view instead of hiding it: the page stays visible and live under the popover, just not interactive, a click in the hole lands on the app view, dismisses the overlay and re-raises the page. Reordering is a synchronous call of about a tenth of a millisecond with no stale frame.

    Migration cost for Seed. The main window is created in one place; about 18 files reference BrowserWindow, 102 uses of .webContents across 12 main-process files, 22 BrowserWindow-typed signatures; fromWebContents needs a small map, ready-to-show has one use. One to two days, mechanical, main process only. The renderer's popovers, menus and dialogs do not change.

Recommendation: the hybrid reorder on a BaseWindow. It gives the visual result wanted (the page stays live under popovers), touches no call sites, needs no second renderer, no child windows and no synthetic input path, and leaves the security model as it is. Native popover windows (the design above) solve the same visual problem with more moving parts. Gate the reorder to macOS until a smoke test on Windows and Linux. If Electron ships per-view pass-through, always-behind becomes a small follow-up.

Interim

Until this ships, the pane keeps hiding under overlays. If the disappearing page becomes a complaint before this project starts, the snapshot option is a day of work and does not conflict with the final design.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime