Electron Multi-Window Patterns
SkillAI & modelsMulti-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Electron Multi-Window Patterns skill
What this skill tells your AI
The instructions your AI receives, as published by agents-inc/skills in src/skills/desktop-multiwindow-electron/SKILL.md and read by ahel’s review.
Quick Guide: Use
BrowserWindowfor single-view windows. UseBaseWindow+WebContentsViewfor multi-view layouts (tabs, split panes, panels).BrowserViewis deprecated since Electron 30 -- migrate toWebContentsView. Track windows with aMap<string, BrowserWindow>registry. Communicate between windows via the main process orMessagePortfor direct renderer-to-renderer channels. Persist window bounds manually or use the upcomingwindowStatePersistenceAPI. Always closewebContentsexplicitly when usingBaseWindow-- unlikeBrowserWindow, it does not auto-cleanup.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST close webContents explicitly when destroying a BaseWindow -- it does not auto-cleanup like BrowserWindow, causing memory leaks)
(You MUST use WebContentsView instead of BrowserView -- BrowserView is deprecated since Electron 30)
(You MUST route all inter-window communication through the main process or MessagePort -- never access another window's renderer directly)
(You MUST validate that saved window bounds are on a visible display before restoring -- monitors may disconnect between sessions)
</critical_requirements>
Auto-detection: multi-window, BaseWindow, WebContentsView, BrowserView migration, contentView, addChildView, removeChildView, parent window, child window, modal window, window registry, MessagePort, MessageChannelMain, window state persistence, screen API, workArea, getAllDisplays, split view, tabs, panels, window lifecycle, ready-to-show, window-all-closed
When to use:
- Creating multi-view layouts (tabs, split panes, embedded panels) with BaseWindow + WebContentsView
- Managing multiple BrowserWindow instances with a window registry
- Migrating from deprecated BrowserView to WebContentsView
- Setting up parent/child or modal windows
- Communicating between windows (via main process relay or MessagePort)
- Persisting and restoring window position, size, and display state
- Placing windows on specific monitors using the screen API
When NOT to use:
- Single-window apps with one view (
BrowserWindowis sufficient on its own) - Choosing a UI framework for the renderer
- IPC patterns between main and a single renderer (basic IPC is outside multi-window scope)
- Styling or layout within a single renderer
Key patterns covered:
- BaseWindow + WebContentsView for multi-view layouts
- BrowserView to WebContentsView migration
- Window lifecycle events (ready-to-show, close, closed)
- Parent/child and modal windows
- Window registry with Map-based tracking
- Inter-window communication via main process and MessagePort
- Window state persistence (bounds, maximized, fullscreen)
- Multi-monitor placement with screen API
Philosophy
Electron's window model has two tiers. BrowserWindow is the simple path: one window, one web view, automatic lifecycle management. BaseWindow + WebContentsView is the flexible path: one window shell containing multiple independently managed web views, each with its own renderer process and preload script.
The key architectural decision: use BrowserWindow for single-view windows, BaseWindow for multi-view layouts. BaseWindow trades convenience for control -- you manage view lifecycle, bounds, and cleanup explicitly.
When to use BaseWindow + WebContentsView:
- Tab bars, split editors, preview panels, embedded browser views
- Any layout where multiple independent web pages share one OS window
- Applications migrating from deprecated BrowserView
When NOT to use BaseWindow:
- Single-view windows (BrowserWindow is simpler and handles cleanup automatically)
- Windows that only need a toolbar or status bar (a single BrowserWindow with HTML layout is sufficient)
Core Patterns
Pattern 1: BaseWindow with WebContentsView
BaseWindow is the window shell; WebContentsView instances are the content. Each view has its own renderer process and preload script.
const { BaseWindow, WebContentsView } = require("electron");
const win = new BaseWindow({ width: 1200, height: 800 });
const sidebar = new WebContentsView({
webPreferences: { preload: path.join(__dirname, "preload.js") },
});
const main = new WebContentsView({
webPreferences: { preload: path.join(__dirname, "preload.js") },
});
win.contentView.addChildView(sidebar);
win.contentView.addChildView(main);
const SIDEBAR_WIDTH = 250;
sidebar.setBounds({ x: 0, y: 0, width: SIDEBAR_WIDTH, height: 800 });
main.setBounds({ x: SIDEBAR_WIDTH, y: 0, width: 950, height: 800 });
sidebar.webContents.loadFile("sidebar.html");
main.webContents.loadFile("main.html");
Key point: Each WebContentsView needs its own webPreferences and preload script. BaseWindow has no webContents of its own. See examples/core.md for complete split-view and tab examples with resize handling.
Pattern 2: BrowserView to WebContentsView Migration
BrowserView is deprecated since Electron 30. Migration is straightforward -- constructors have the same shape.
| Deprecated (BrowserView) | Replacement (WebContentsView) |
|---|---|
new BrowserView(opts) | new WebContentsView(opts) |
win.addBrowserView(view) | win.contentView.addChildView(view) |
win.removeBrowserView(view) | win.contentView.removeChildView(view) |
win.getBrowserViews() | win.contentView.children |
win.setTopBrowserView(view) | win.contentView.addChildView(view) (re-adding reorders) |
view.setAutoResize({ vertical: true }) | Manual resize via win.on("resize", ...) + setBounds() |
Gotcha: WebContentsView defaults to a white background; BrowserView defaulted to transparent. Set view.setBackgroundColor("#00000000") for transparency.
See examples/core.md for the complete migration pattern with auto-resize replacement.
Pattern 3: Window Lifecycle Events
Window events fire in a predictable order. Use ready-to-show to prevent visual flash, close to intercept (confirmations, state saving), closed for final cleanup.
const win = new BrowserWindow({ show: false });
// Prevent white flash -- show only after first paint
win.once("ready-to-show", () => {
win.show();
});
// Intercept close for unsaved changes
win.on("close", (event) => {
if (hasUnsavedChanges()) {
event.preventDefault();
promptSaveDialog(win);
}
});
// Final cleanup after window is gone
win.on("closed", () => {
windowRegistry.delete(win.id);
});
Gotcha: ready-to-show fires on BrowserWindow (which owns a webContents) but NOT on BaseWindow (which has no webContents). For BaseWindow, listen on the individual WebContentsView's webContents instead: view.webContents.once("ready-to-show", ...).
See examples/core.md for the full lifecycle sequence and BaseWindow workaround.
Pattern 4: Parent/Child and Modal Windows
Child windows always appear above their parent. Modal windows additionally disable the parent until closed.
const parent = new BrowserWindow({ width: 800, height: 600 });
// Child window: always on top of parent, non-blocking
const child = new BrowserWindow({
parent,
width: 400,
height: 300,
});
// Modal window: blocks parent interaction
const modal = new BrowserWindow({
parent,
modal: true,
show: false,
width: 500,
height: 400,
});
modal.once("ready-to-show", () => modal.show());
Platform behavior: On macOS, modal child windows display as sheets attached to the parent. On Windows/Linux, they display as separate windows with the parent disabled.
See examples/core.md for confirmation dialogs and settings windows.
Pattern 5: Window Registry
Track all open windows with a Map for reliable lookup, messaging, and cleanup.
const windowRegistry = new Map();
function createWindow(id, options) {
const win = new BrowserWindow(options);
windowRegistry.set(id, win);
win.on("closed", () => {
windowRegistry.delete(id);
});
return win;
}
// Find and focus a window by ID
function focusWindow(id) {
const win = windowRegistry.get(id);
if (!win) return;
if (win.isMinimized()) win.restore();
win.focus();
}
Key point: Use string IDs (not window objects) as keys. Clean up on closed event. The registry enables "show existing or create new" patterns for settings, about, and preferences windows.
See examples/core.md for the full singleton window pattern.
Pattern 6: Inter-Window Communication
Two patterns: main process relay for simple messages, MessagePort for direct high-frequency renderer-to-renderer channels.
// Pattern A: Main process relay
ipcMain.on("message-to-window", (_event, targetId, channel, data) => {
const target = windowRegistry.get(targetId);
if (target) target.webContents.send(channel, data);
});
// Pattern B: MessagePort -- direct renderer-to-renderer
const { MessageChannelMain } = require("electron");
const { port1, port2 } = new MessageChannelMain();
window1.webContents.postMessage("port", null, [port1]);
window2.webContents.postMessage("port", null, [port2]);
Key point: Main process relay is simpler but adds latency. MessagePort creates a direct channel after initial setup. Use postMessage (not send) to transfer ports.
See examples/inter-window-communication.md for complete examples of both patterns.
Pattern 7: Window State Persistence
Save and restore window bounds, maximized state, and display information across sessions.
function saveWindowState(win, stateFile) {
const bounds = win.getBounds();
const state = {
bounds,
isMaximized: win.isMaximized(),
isFullScreen: win.isFullScreen(),
displayId: screen.getDisplayMatching(bounds).id,
};
fs.writeFileSync(stateFile, JSON.stringify(state));
}
Key point: Always validate restored bounds against current displays -- a monitor may have been disconnected. Fall back to the primary display's work area if the saved display is unavailable.
See examples/core.md for the complete save/restore cycle with multi-monitor validation.
Pattern 8: Multi-Monitor Placement
Use the screen API to enumerate displays, find work areas, and place windows on specific monitors.
const { screen } = require("electron");
const displays = screen.getAllDisplays();
const externalDisplay = displays.find(
(d) => d.bounds.x !== 0 || d.bounds.y !== 0,
);
if (externalDisplay) {
const win = new BrowserWindow({
x: externalDisplay.bounds.x,
y: externalDisplay.bounds.y,
width: 800,
height: 600,
});
}
Key point: Use workArea (not bounds) to avoid placing windows behind taskbars/docks. Listen for display-added, display-removed, and display-metrics-changed events to react to monitor changes at runtime.
See examples/core.md for display enumeration and safe placement.
<decision_framework>
Decision Framework
Window Type Selection
How many web views does this window need?
+-- One full-size view?
| +-- BrowserWindow (simpler, automatic lifecycle)
+-- Multiple views (tabs, split pane, sidebar + content)?
| +-- BaseWindow + WebContentsView
+-- Frameless window with custom layout?
+-- One view? -> BrowserWindow with frame: false
+-- Multiple views? -> BaseWindow with frame: false
Inter-Window Communication
How should windows communicate?
+-- Simple, infrequent messages?
| +-- Main process relay (ipcMain/webContents.send)
+-- High-frequency or streaming data?
| +-- MessagePort (direct renderer-to-renderer after setup)
+-- Shared state across windows?
+-- Main process as single source of truth, push updates via IPC
Window Relationship
What is the relationship between windows?
+-- Independent (editor, browser tabs)?
| +-- Separate BrowserWindow instances, window registry
+-- Always above parent (inspector, palette)?
| +-- Child window: { parent: parentWin }
+-- Blocks parent (save dialog, settings confirmation)?
+-- Modal window: { parent: parentWin, modal: true }
</decision_framework>
Detailed resources:
- examples/core.md - BaseWindow + WebContentsView, lifecycle, registry, state persistence, multi-monitor
- examples/inter-window-communication.md - Main process relay, MessagePort, typed channels
- reference.md - API quick-reference tables, migration checklist, event order
<red_flags>
RED FLAGS
Critical Issues:
- Not closing
webContentswhen destroying aBaseWindow-- causes memory leaks (BrowserWindow auto-cleans, BaseWindow does not) - Using deprecated
BrowserViewinstead ofWebContentsView-- deprecated since Electron 30 - Direct renderer-to-renderer communication bypassing the main process -- violates process isolation
- Restoring window bounds without checking if the target display still exists -- window appears off-screen
Architecture Issues:
- Using
BaseWindowfor single-view windows -- unnecessary complexity, useBrowserWindow - Using
BrowserView.setAutoResize()patterns withWebContentsView-- no equivalent exists, use manual resize listeners - Storing
BrowserWindowobjects as Map values without cleaning up onclosed-- stale references - Creating child windows from the renderer process -- always create from main
Common Mistakes:
- Expecting
ready-to-showonBaseWindow-- it fires onBrowserWindowonly; forBaseWindow, listen onview.webContents - Forgetting that
WebContentsViewdefaults to white background (BrowserView defaulted to transparent) -- set"#00000000"explicitly - Using
ipcRenderer.send()to transferMessagePort-- onlypostMessage()can transfer ports - Placing windows using
display.boundsinstead ofdisplay.workArea-- window ends up behind taskbar/dock - Not handling
display-removedevent -- window references a disconnected monitor
Gotchas & Edge Cases:
- Re-adding a child view with
addChildView()moves it to the top of the z-order -- this is intentional, not a bug setBounds()coordinates are relative to the parent view, not the screen- On macOS, modal windows display as sheets attached to the parent window
win.getBounds()returns the outer frame bounds on some platforms -- content area may differ- Each
WebContentsViewruns its own renderer process -- resource usage scales linearly with view count MessagePortMainrequires calling.start()before messages are delivered -- they queue until then
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST close webContents explicitly when destroying a BaseWindow -- it does not auto-cleanup like BrowserWindow, causing memory leaks)
(You MUST use WebContentsView instead of BrowserView -- BrowserView is deprecated since Electron 30)
(You MUST route all inter-window communication through the main process or MessagePort -- never access another window's renderer directly)
(You MUST validate that saved window bounds are on a visible display before restoring -- monitors may disconnect between sessions)
Failure to follow these rules will cause memory leaks, deprecated API warnings, broken inter-process communication, or off-screen windows.
</critical_reminders>
Signals
- GitHub stars
- 24
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
desktop-multiwindow-electron- Source
- github.com/agents-inc/skills