Electron
Electron fundamentals for building cross-platform desktop applications with web technologies
You are an expert in Electron for building cross-platform desktop applications using HTML, CSS, and JavaScript. ## Key Points - **Main process**: Entry point, runs Node.js, manages `BrowserWindow` instances - **Renderer process**: Each window runs in its own renderer process (a Chromium web page) - **Preload scripts**: Bridge between main and renderer with controlled Node.js access - Always enable `contextIsolation: true` and disable `nodeIntegration` in renderer processes. - Use preload scripts as the controlled bridge between main and renderer. - Prevent white flash by creating windows with `show: false` and displaying on `ready-to-show`. - Use `app.requestSingleInstanceLock()` to prevent multiple app instances. - Handle the macOS `activate` event to recreate windows when the dock icon is clicked. - Store window state (size, position) using `electron-store` or a similar library and restore on launch. - Use `webContents.openDevTools()` only in development; remove or gate behind a flag in production. - **Forgetting platform differences**: macOS keeps apps running after all windows close; Windows and Linux do not. Always handle `window-all-closed` appropriately. - **Enabling `nodeIntegration` in renderers**: This exposes full Node.js APIs to any content loaded in the window, including remote content. Never do this. ## Quick Example ```bash mkdir my-electron-app && cd my-electron-app npm init -y npm install --save-dev electron ```
skilldb get desktop-app-skills/ElectronFull skill: 242 linesElectron — Desktop Apps
You are an expert in Electron for building cross-platform desktop applications using HTML, CSS, and JavaScript.
Overview
Electron combines Chromium and Node.js into a single runtime, allowing developers to build desktop applications using web technologies. Each Electron app runs a main process (Node.js) and one or more renderer processes (Chromium browser windows). The main process manages application lifecycle, creates windows, and accesses native APIs. Renderer processes display the UI.
Key architecture:
- Main process: Entry point, runs Node.js, manages
BrowserWindowinstances - Renderer process: Each window runs in its own renderer process (a Chromium web page)
- Preload scripts: Bridge between main and renderer with controlled Node.js access
Setup & Configuration
Project initialization
mkdir my-electron-app && cd my-electron-app
npm init -y
npm install --save-dev electron
Minimal project structure
my-electron-app/
├── main.js # Main process entry
├── preload.js # Preload script
├── index.html # Renderer UI
├── renderer.js # Renderer logic
└── package.json
package.json configuration
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --enable-logging"
},
"devDependencies": {
"electron": "^33.0.0"
}
}
Main process entry (main.js)
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
// macOS: re-create window when dock icon clicked and no windows open
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Preload script (preload.js)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => process.versions.electron,
send: (channel, data) => {
const validChannels = ['toMain'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, callback) => {
const validChannels = ['fromMain'];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (_event, ...args) => callback(...args));
}
},
});
Core Patterns
BrowserWindow options
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
frame: true, // set false for frameless window
titleBarStyle: 'hiddenInset', // macOS traffic-light style
transparent: false,
resizable: true,
show: false, // prevent flash of white on load
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
// Show when ready to prevent visual flash
win.once('ready-to-show', () => {
win.show();
});
Loading content
// Load a local HTML file
win.loadFile('index.html');
// Load a remote URL
win.loadURL('https://example.com');
// Load with query params
win.loadFile('index.html', {
query: { page: 'settings' },
});
App lifecycle events
app.on('ready', () => { /* app initialized */ });
app.on('window-all-closed', () => { /* all windows closed */ });
app.on('activate', () => { /* macOS dock click */ });
app.on('before-quit', () => { /* about to quit */ });
app.on('will-quit', () => { /* final cleanup */ });
// Single instance lock
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', (_event, argv) => {
// Focus the existing window
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
Managing multiple windows
const windows = new Map();
function createWindow(id, options = {}) {
const win = new BrowserWindow({
width: 800,
height: 600,
...options,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
windows.set(id, win);
win.on('closed', () => windows.delete(id));
return win;
}
Best Practices
- Always enable
contextIsolation: trueand disablenodeIntegrationin renderer processes. - Use preload scripts as the controlled bridge between main and renderer.
- Prevent white flash by creating windows with
show: falseand displaying onready-to-show. - Use
app.requestSingleInstanceLock()to prevent multiple app instances. - Handle the macOS
activateevent to recreate windows when the dock icon is clicked. - Store window state (size, position) using
electron-storeor a similar library and restore on launch. - Use
webContents.openDevTools()only in development; remove or gate behind a flag in production.
Common Pitfalls
- Forgetting platform differences: macOS keeps apps running after all windows close; Windows and Linux do not. Always handle
window-all-closedappropriately. - Enabling
nodeIntegrationin renderers: This exposes full Node.js APIs to any content loaded in the window, including remote content. Never do this. - Memory leaks from undisposed windows: Always clean up references when windows are closed.
- Blocking the main process: Heavy computation in the main process freezes all windows. Use worker threads or offload to the renderer.
- Shipping with devtools open: Ensure
openDevTools()calls are gated behind!app.isPackagedor an environment variable. - Not handling
ready-to-show: Users see a white window flash before content loads.
Core Philosophy
Electron's power is its flexibility — combining the full Node.js runtime with a Chromium renderer gives you access to both web and native capabilities. But this power demands discipline. The main process is your application's backbone: it manages windows, handles system events, and mediates all access to native APIs. Keep it lean, responsive, and free from heavy computation. The renderer process handles the UI, and the preload script is the carefully controlled bridge between them.
Security must be the foundation, not an afterthought. Always enable contextIsolation, disable nodeIntegration, and use preload scripts to expose a minimal, validated API surface. An Electron app with nodeIntegration: true is a web page with root access to the user's machine — any XSS vulnerability becomes a full system compromise.
Think like a desktop developer, not just a web developer. Handle platform-specific behaviors (macOS dock behavior, Windows taskbar integration, Linux desktop entries), manage window state and position, implement single-instance locking, and respond to system events like sleep, resume, and power management. The details that make an Electron app feel native are the ones web developers often forget.
Anti-Patterns
-
Enabling
nodeIntegrationin renderer processes — this exposes the full Node.js API to any content loaded in the window, including remote URLs; never do this in production. -
Performing heavy computation in the main process — CPU-intensive work in the main process blocks all windows simultaneously; offload to worker threads, utility processes, or the renderer.
-
Creating windows without
show: falseandready-to-show— users see an ugly white flash while the window loads content; hide the window initially and show it only when rendering is complete. -
Forgetting platform differences in lifecycle management — macOS keeps apps running when all windows are closed; Windows and Linux quit; not handling
window-all-closedandactivateappropriately makes the app feel broken on each platform. -
Shipping with DevTools enabled in production — leaving
openDevTools()calls ungated exposes application internals to end users; always guard behind!app.isPackagedor an environment flag.
Install this skill directly: skilldb add desktop-app-skills
Related Skills
Auto Update
Auto-update mechanisms for desktop applications using electron-updater and Tauri's built-in updater
Electron Ipc
IPC communication patterns for Electron main and renderer process messaging
File System Access
File system access patterns, native dialogs, and drag-and-drop for desktop applications
Native Menus
System tray icons, native application menus, and desktop notifications for Electron and Tauri apps
Packaging
Building, packaging, and distributing desktop applications for Windows, macOS, and Linux
Security
Desktop application security including context isolation, preload scripts, CSP, and sandboxing for Electron and Tauri