Multi-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence
Scanned 9/5/2026
Install to Claude Code
npx -y skills add agents-inc/skills --skill desktop-multiwindow-electron --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Desktop Multiwindow Electron?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/agents-inc-desktop-multiwindow-electron)More formats (shields.io, HTML) on the badges page.
---
name: desktop-multiwindow-electron
description: Multi-window management, WebContentsView, BaseWindow, window lifecycle, inter-window communication, state persistence
---
# Electron Multi-Window Patterns
> **Quick Guide:** Use `BrowserWindow` for single-view windows. Use `BaseWindow` + `WebContentsView` for multi-view layouts (tabs, split panes, panels). `BrowserView` is deprecated since Electron 30 -- migrate to `WebContentsView`. Track windows with a `Map<string, BrowserWindow>` registry. Communicate between windows via the main process or `MessagePort` for direct renderer-to-renderer channels. Persist window bounds manually or use the upcoming `windowStatePersistence` API. Always close `webContents` explicitly when using `BaseWindow` -- unlike `BrowserWindow`, 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 (`BrowserWindow` is 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>
## 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)
</philosophy>
---
<patterns>
## 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.
```javascript
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](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](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.
```javascript
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](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.
```javascript
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](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.
```javascript
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](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.
```javascript
// 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](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.
```javascript
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](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.
```javascript
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](examples/core.md) for display enumeration and safe placement.
</patterns>
---
<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](examples/core.md) - BaseWindow + WebContentsView, lifecycle, registry, state persistence, multi-monitor
- [examples/inter-window-communication.md](examples/inter-window-communication.md) - Main process relay, MessagePort, typed channels
- [reference.md](reference.md) - API quick-reference tables, migration checklist, event order
---
<red_flags>
## RED FLAGS
**Critical Issues:**
- Not closing `webContents` when destroying a `BaseWindow` -- causes memory leaks (BrowserWindow auto-cleans, BaseWindow does not)
- Using deprecated `BrowserView` instead of `WebContentsView` -- 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 `BaseWindow` for single-view windows -- unnecessary complexity, use `BrowserWindow`
- Using `BrowserView.setAutoResize()` patterns with `WebContentsView` -- no equivalent exists, use manual resize listeners
- Storing `BrowserWindow` objects as Map values without cleaning up on `closed` -- stale references
- Creating child windows from the renderer process -- always create from main
**Common Mistakes:**
- Expecting `ready-to-show` on `BaseWindow` -- it fires on `BrowserWindow` only; for `BaseWindow`, listen on `view.webContents`
- Forgetting that `WebContentsView` defaults to white background (BrowserView defaulted to transparent) -- set `"#00000000"` explicitly
- Using `ipcRenderer.send()` to transfer `MessagePort` -- only `postMessage()` can transfer ports
- Placing windows using `display.bounds` instead of `display.workArea` -- window ends up behind taskbar/dock
- Not handling `display-removed` event -- 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 `WebContentsView` runs its own renderer process -- resource usage scales linearly with view count
- `MessagePortMain` requires 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>
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!