Skip to main content
Technology & EngineeringDesktop App242 lines

Electron

Electron fundamentals for building cross-platform desktop applications with web technologies

Quick Summary26 lines
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 lines
Paste into your CLAUDE.md or agent config

Electron — 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 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

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: 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.

Common Pitfalls

  • 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.
  • 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.isPackaged or 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 nodeIntegration in 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: false and ready-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-closed and activate appropriately 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.isPackaged or an environment flag.

Install this skill directly: skilldb add desktop-app-skills

Get CLI access →