Getting started
Introduction
Electron combines Chromium and Node.js into a single runtime. Build desktop apps for Windows, macOS, and Linux using web technologies.
Current Version: 40.2.0
Quick Start
# Create new app with Electron Forge
npm init electron-app@latest my-app
# Navigate to app
cd my-app
# Start development
npm start
# Build distributable
npm run make
Minimal App Structure
my-app/
├── package.json
├── main.js # Main process
├── preload.js # Preload script
└── index.html # Renderer UI
Main entry in package.json:
{
"main": "main.js"
}
Basic main.js
const { app, BrowserWindow } = require("electron");
const path = require("node:path");
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile("index.html");
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
Process Architecture
Main Process
Single process that runs in Node.js environment. Controls app lifecycle and creates renderer processes.
// main.js - Main process entry point
const { app, BrowserWindow } = require("electron");
app.whenReady().then(() => {
// Create windows, setup IPC, etc.
});
Capabilities:
- Full Node.js API access
- Create BrowserWindows
- Handle IPC communication
- Access native OS features
- Control app lifecycle
Renderer Process
One per BrowserWindow. Runs in Chromium environment with web standards (HTML/CSS/JS).
<!-- index.html - Renderer process -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<h1>Hello Electron!</h1>
<script src="./renderer.js"></script>
</body>
</html>
Capabilities:
- DOM manipulation
- Web APIs (fetch, localStorage, etc.)
- Limited Node.js access (via preload)
- Isolated from main process
Preload Scripts
Runs before renderer, bridges main and renderer with controlled API exposure.
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (channel, data) => {
ipcRenderer.send(channel, data);
},
onReply: (callback) => {
ipcRenderer.on("reply", (_event, value) => callback(value));
},
});
Purpose:
- Expose safe APIs to renderer
- Enforce security boundaries
- Validate IPC messages
IPC Communication
Renderer → Main (One-way)
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
send: (channel, data) => ipcRenderer.send(channel, data),
});
// main.js
const { ipcMain } = require("electron");
ipcMain.on("message-channel", (event, data) => {
console.log("Received:", data);
});
// renderer.js
window.electronAPI.send("message-channel", {
text: "Hello from renderer",
});
Renderer → Main (Two-way)
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
invoke: (channel, data) => ipcRenderer.invoke(channel, data),
});
// main.js
ipcMain.handle("get-data", async (event, arg) => {
const result = await fetchData(arg);
return result;
});
// renderer.js
const data = await window.electronAPI.invoke("get-data", "param");
console.log(data);
Main → Renderer
// main.js
const { BrowserWindow } = require("electron");
const win = BrowserWindow.getFocusedWindow();
win.webContents.send("update", { data: "New data" });
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
onUpdate: (callback) => {
ipcRenderer.on("update", (_event, value) => callback(value));
},
});
// renderer.js
window.electronAPI.onUpdate((data) => {
console.log("Update received:", data);
});
IPC Channel Validation
// preload.js - Whitelist channels
const VALID_CHANNELS = ["message", "data-request"];
contextBridge.exposeInMainWorld("electronAPI", {
send: (channel, data) => {
if (VALID_CHANNELS.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
});
// main.js - Validate sender
ipcMain.on("message", (event, data) => {
// Verify sender is from expected window
if (event.senderFrame === mainWindow.webContents.mainFrame) {
processMessage(data);
}
});
BrowserWindow
Window Creation
const { BrowserWindow } = require("electron");
const win = new BrowserWindow({
width: 1024,
height: 768,
minWidth: 800,
minHeight: 600,
show: false, // Don't show until ready
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
win.loadFile("index.html");
Window Events
// Show window when ready
win.once("ready-to-show", () => {
win.show();
});
// Handle close
win.on("close", (event) => {
// Prevent close, show confirmation
event.preventDefault();
dialog
.showMessageBox({
type: "question",
buttons: ["Yes", "No"],
message: "Are you sure you want to quit?",
})
.then(({ response }) => {
if (response === 0) win.destroy();
});
});
// Cleanup on closed
win.on("closed", () => {
win = null;
});
// Window focus events
win.on("focus", () => console.log("Focused"));
win.on("blur", () => console.log("Blurred"));
Window Methods
// Load content
win.loadFile("index.html");
win.loadURL("https://example.com");
// Window state
win.maximize();
win.minimize();
win.restore();
win.close();
// Window properties
win.setTitle("New Title");
win.setSize(800, 600);
win.center();
// Developer tools
win.webContents.openDevTools();
win.webContents.closeDevTools();
Frameless Window
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
titleBarStyle: "hidden", // macOS only
transparent: true,
});
Custom titlebar with draggable region:
/* styles.css */
.titlebar {
-webkit-app-region: drag;
height: 32px;
}
.titlebar button {
-webkit-app-region: no-drag;
}
Security
Security Checklist
✅ MUST Enable:
webPreferences: {
contextIsolation: true, // ✅ Isolate contexts
sandbox: true, // ✅ Enable sandbox
nodeIntegration: false, // ✅ Disable node in renderer
enableRemoteModule: false // ✅ Disable remote
}
❌ MUST Disable:
webPreferences: {
nodeIntegration: true, // ❌ NEVER for remote content
webSecurity: false, // ❌ NEVER disable
allowRunningInsecureContent: true // ❌ NEVER allow
}
Content Security Policy:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
Validate All Inputs
// ❌ BAD - No validation
ipcMain.handle("open-file", async (event, filePath) => {
return fs.readFileSync(filePath);
});
// ✅ GOOD - Validate path
ipcMain.handle("open-file", async (event, filePath) => {
const safeDir = app.getPath("userData");
const resolvedPath = path.resolve(safeDir, filePath);
if (!resolvedPath.startsWith(safeDir)) {
throw new Error("Invalid path");
}
return fs.readFileSync(resolvedPath);
});
Secure URL Loading
// ❌ BAD - User input directly
win.loadURL(userInput);
// ✅ GOOD - Validate URLs
const { URL } = require("url");
function loadSafeURL(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== "https:") {
throw new Error("Only HTTPS allowed");
}
// Whitelist domains
const allowedHosts = ["example.com", "api.example.com"];
if (!allowedHosts.includes(url.hostname)) {
throw new Error("Domain not allowed");
}
win.loadURL(url.toString());
} catch (err) {
console.error("Invalid URL:", err);
}
}
Navigation Protection
// Prevent navigation to untrusted sites
win.webContents.on("will-navigate", (event, url) => {
const { URL } = require("url");
const parsedUrl = new URL(url);
if (parsedUrl.origin !== "https://example.com") {
event.preventDefault();
}
});
// Prevent new window creation
win.webContents.setWindowOpenHandler(({ url }) => {
// Open in default browser instead
shell.openExternal(url);
return { action: "deny" };
});
Native Dialogs
File Open Dialog
const { dialog } = require("electron");
// Single file
const result = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{ name: "Images", extensions: ["jpg", "png", "gif"] },
{ name: "All Files", extensions: ["*"] },
],
});
if (!result.canceled) {
console.log(result.filePaths[0]);
}
// Multiple files
const result = await dialog.showOpenDialog({
properties: ["openFile", "multiSelections"],
});
// Directory selection
const result = await dialog.showOpenDialog({
properties: ["openDirectory"],
});
Save Dialog
const result = await dialog.showSaveDialog({
title: "Save File",
defaultPath: "document.txt",
filters: [
{ name: "Text Files", extensions: ["txt"] },
{ name: "All Files", extensions: ["*"] },
],
});
if (!result.canceled) {
fs.writeFileSync(result.filePath, content);
}
Message Box
// Info dialog
await dialog.showMessageBox({
type: "info",
title: "Information",
message: "Operation completed",
detail: "Additional details here",
});
// Confirmation dialog
const result = await dialog.showMessageBox({
type: "question",
buttons: ["Yes", "No", "Cancel"],
defaultId: 0,
cancelId: 2,
message: "Save changes?",
detail: "You have unsaved changes",
});
if (result.response === 0) {
// Yes clicked
}
Error Box
// Synchronous error dialog
dialog.showErrorBox("Error Title", "Error message details");
Types: none, info, error, question, warning
Menu & Tray
Application Menu
const { Menu, app } = require("electron");
const template = [
{
label: "File",
submenu: [
{
label: "New",
accelerator: "CmdOrCtrl+N",
click: () => createNewFile(),
},
{
label: "Open",
accelerator: "CmdOrCtrl+O",
click: () => openFile(),
},
{ type: "separator" },
{ role: "quit" },
],
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
Context Menu
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
showContextMenu: () => ipcRenderer.send("show-context-menu"),
});
// main.js
const { Menu, ipcMain } = require("electron");
const contextMenu = Menu.buildFromTemplate([
{ label: "Copy", role: "copy" },
{ label: "Paste", role: "paste" },
{ type: "separator" },
{ label: "Custom Action", click: () => console.log("Clicked") },
]);
ipcMain.on("show-context-menu", (event) => {
contextMenu.popup(BrowserWindow.fromWebContents(event.sender));
});
// renderer.js
window.addEventListener("contextmenu", (e) => {
e.preventDefault();
window.electronAPI.showContextMenu();
});
System Tray
const { Tray, Menu, nativeImage } = require("electron");
let tray = null;
app.whenReady().then(() => {
const icon = nativeImage.createFromPath("assets/icon.png");
tray = new Tray(icon.resize({ width: 16, height: 16 }));
const contextMenu = Menu.buildFromTemplate([
{ label: "Show App", click: () => mainWindow.show() },
{ type: "separator" },
{ label: "Quit", click: () => app.quit() },
]);
tray.setToolTip("My Electron App");
tray.setContextMenu(contextMenu);
tray.on("click", () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
});
Menu Roles
Built-in roles for common actions:
{
role: "undo";
}
{
role: "redo";
}
{
role: "cut";
}
{
role: "copy";
}
{
role: "paste";
}
{
role: "selectAll";
}
{
role: "reload";
}
{
role: "toggleDevTools";
}
{
role: "quit";
}
{
role: "minimize";
}
{
role: "close";
}
{
role: "help";
}
{
role: "about";
}
{
role: "services";
} // macOS only
{
role: "hide";
} // macOS only
App Lifecycle
Lifecycle Events
const { app } = require("electron");
// App is ready to create windows
app.on("ready", () => {
console.log("App ready");
});
// Alternative to 'ready'
app.whenReady().then(() => {
console.log("App ready (promise)");
});
// All windows closed
app.on("window-all-closed", () => {
// Quit on all platforms except macOS
if (process.platform !== "darwin") {
app.quit();
}
});
// macOS: Re-activate (dock icon clicked)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Before app quits
app.on("before-quit", (event) => {
// Cleanup, save state
console.log("Before quit");
});
// App will quit
app.on("will-quit", (event) => {
// Final cleanup
console.log("Will quit");
});
// App quit
app.on("quit", (event, exitCode) => {
console.log("Quit with code:", exitCode);
});
Preventing Quit
// Prevent quit on close
app.on("before-quit", (event) => {
if (!readyToQuit) {
event.preventDefault();
// Perform async cleanup
performCleanup().then(() => {
readyToQuit = true;
app.quit();
});
}
});
Single Instance Lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, argv, workingDir) => {
// Focus existing window
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.whenReady().then(() => {
createWindow();
});
}
App Paths
// Get common paths
app.getPath("home"); // User's home
app.getPath("appData"); // App data
app.getPath("userData"); // User data for app
app.getPath("temp"); // Temp directory
app.getPath("downloads"); // Downloads
app.getPath("documents"); // Documents
app.getPath("desktop"); // Desktop
app.getPath("logs"); // Logs
// Set custom user data path
app.setPath("userData", "/path/to/custom/dir");
Building & Packaging
Electron Forge Setup
# Create new app
npm init electron-app@latest my-app
# With template
npm init electron-app@latest my-app -- \
--template=webpack
# Available templates
# - webpack
# - webpack-typescript
# - vite
# - vite-typescript
Project Scripts
{
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish"
}
}
# Development
npm start
# Create distributable
npm run make
# Publish to distribution service
npm run publish
Forge Configuration
// forge.config.js
module.exports = {
packagerConfig: {
asar: true,
icon: "./assets/icon",
appBundleId: "com.myapp.id",
appCategoryType: "public.app-category.utilities",
},
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
// Windows installer config
},
},
{
name: "@electron-forge/maker-dmg",
config: {
// macOS DMG config
},
},
{
name: "@electron-forge/maker-deb",
config: {
// Linux Debian config
},
},
],
};
Auto-Update Setup
const { autoUpdater } = require("electron-updater");
app.whenReady().then(() => {
// Check for updates on startup
autoUpdater.checkForUpdatesAndNotify();
// Check periodically
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify();
}, 60000 * 60); // Every hour
});
autoUpdater.on("update-available", () => {
console.log("Update available");
});
autoUpdater.on("update-downloaded", () => {
dialog
.showMessageBox({
type: "info",
title: "Update Ready",
message: "A new version is ready. Restart now?",
buttons: ["Restart", "Later"],
})
.then((result) => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
Debugging
Renderer DevTools
// Open DevTools programmatically
mainWindow.webContents.openDevTools();
// Open in detached mode
mainWindow.webContents.openDevTools({ mode: "detach" });
// Auto-open on window creation
const win = new BrowserWindow({
webPreferences: {
devTools: true,
},
});
win.webContents.openDevTools();
Keyboard shortcut: Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows/Linux)
Main Process Debugging
# Start with inspector
electron --inspect=5858 .
# Break on first line
electron --inspect-brk=5858 .
Connect Chrome DevTools:
- Open
chrome://inspectin Chrome - Click "Configure" → Add
localhost:5858 - Click "inspect" under Remote Target
VSCode Configuration
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": ["."],
"outputCapture": "std"
},
{
"name": "Debug Renderer Process",
"type": "chrome",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"runtimeArgs": [".", "--remote-debugging-port=9223"],
"webRoot": "${workspaceFolder}"
}
]
}
Logging & Debug Output
// Enable Electron debug logs
app.commandLine.appendSwitch("enable-logging");
app.commandLine.appendSwitch("v", "1");
// Custom logging
const log = require("electron-log");
log.info("Info message");
log.warn("Warning message");
log.error("Error message");
// Logs location
console.log(log.transports.file.getFile());
Common Patterns
Loading Screen
let mainWindow;
let loadingWindow;
function createLoadingWindow() {
loadingWindow = new BrowserWindow({
width: 300,
height: 400,
frame: false,
transparent: true,
});
loadingWindow.loadFile("loading.html");
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
show: false,
});
mainWindow.loadFile("index.html");
mainWindow.once("ready-to-show", () => {
loadingWindow.close();
mainWindow.show();
});
}
app.whenReady().then(() => {
createLoadingWindow();
setTimeout(() => {
createMainWindow();
}, 1000);
});
Store User Data
const Store = require("electron-store");
const store = new Store();
// Set values
store.set("preferences.theme", "dark");
store.set("windowBounds", { width: 800, height: 600 });
// Get values
const theme = store.get("preferences.theme");
const bounds = store.get("windowBounds", { width: 800, height: 600 });
// Delete
store.delete("preferences.theme");
// Clear all
store.clear();
// Get store path
console.log(store.path);
Deep Linking
// Protocol registration (macOS/Windows)
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient("myapp", process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient("myapp");
}
// Handle deep link
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, argv, workingDir) => {
// Windows/Linux deep link
const url = argv.find((arg) => arg.startsWith("myapp://"));
if (url) handleDeepLink(url);
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
// macOS deep link
app.on("open-url", (event, url) => {
event.preventDefault();
handleDeepLink(url);
});
}
function handleDeepLink(url) {
console.log("Deep link:", url);
// myapp://action/param
}
Native Notifications
// Main process
const { Notification } = require("electron");
function showNotification(title, body) {
new Notification({
title: title,
body: body,
icon: path.join(__dirname, "icon.png"),
}).show();
}
// Renderer process (HTML5 Notification API)
if (Notification.permission === "granted") {
new Notification("Title", {
body: "Message body",
icon: "icon.png",
});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
new Notification("Title", { body: "Message" });
}
});
}
Global Shortcuts
const { globalShortcut } = require("electron");
app.whenReady().then(() => {
// Register shortcut
globalShortcut.register("CommandOrControl+X", () => {
console.log("CommandOrControl+X pressed");
mainWindow.show();
});
// Check if registered
const isRegistered = globalShortcut.isRegistered("CommandOrControl+X");
console.log("Registered:", isRegistered);
});
app.on("will-quit", () => {
// Unregister specific shortcut
globalShortcut.unregister("CommandOrControl+X");
// Unregister all shortcuts
globalShortcut.unregisterAll();
});
Advanced Features
Screen Capture
const { desktopCapturer } = require("electron");
async function getScreenSources() {
const sources = await desktopCapturer.getSources({
types: ["window", "screen"],
thumbnailSize: { width: 150, height: 150 },
});
return sources.map((source) => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
}));
}
// In renderer (with preload bridge)
const sources = await window.electronAPI.getScreenSources();
sources.forEach((source) => {
console.log(source.name);
});
Power Management
const { powerMonitor, powerSaveBlocker } = require("electron");
app.whenReady().then(() => {
// Monitor power events
powerMonitor.on("suspend", () => {
console.log("System going to sleep");
});
powerMonitor.on("resume", () => {
console.log("System woke up");
});
powerMonitor.on("on-ac", () => {
console.log("Plugged in");
});
powerMonitor.on("on-battery", () => {
console.log("On battery");
});
// Prevent sleep
const id = powerSaveBlocker.start("prevent-app-suspension");
// Later, allow sleep
powerSaveBlocker.stop(id);
});
Print to PDF
// Main process
ipcMain.handle("print-pdf", async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender);
const data = await win.webContents.printToPDF({
marginsType: 0,
pageSize: "A4",
printBackground: true,
landscape: false,
});
const pdfPath = path.join(app.getPath("downloads"), "output.pdf");
fs.writeFileSync(pdfPath, data);
return pdfPath;
});
// Renderer
const pdfPath = await window.electronAPI.printPDF();
console.log("PDF saved to:", pdfPath);
Custom Protocol
const { protocol } = require("electron");
app.whenReady().then(() => {
protocol.handle("myapp", (request) => {
const url = request.url.substr(8); // Remove 'myapp://'
const filePath = path.join(__dirname, "assets", url);
return net.fetch("file://" + filePath);
});
});
// Use in renderer
win.loadURL("myapp://index.html");
Gotchas
macOS App Activation
On macOS, apps stay active even when all windows are closed. Always check for zero windows on activate event:
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Context Isolation Breaking Changes
Since Electron 12, contextIsolation: true is default. Direct require() in renderer no longer works:
// ❌ No longer works (even with nodeIntegration: true)
const fs = require("fs"); // Error in renderer
// ✅ Use preload + contextBridge
// preload.js
contextBridge.exposeInMainWorld("fs", {
readFile: (path) => fs.readFileSync(path, "utf-8"),
});
Remote Module Deprecated
@electron/remote is deprecated. Use IPC instead:
// ❌ Old way (deprecated)
const { BrowserWindow } = require("@electron/remote");
const win = new BrowserWindow();
// ✅ New way (IPC)
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
createWindow: () => ipcRenderer.invoke("create-window"),
});
// main.js
ipcMain.handle("create-window", () => {
const win = new BrowserWindow({ width: 800, height: 600 });
});
ASAR Archive Path Issues
When packaged, use __dirname carefully with asar archives:
// ❌ May fail in production
const iconPath = __dirname + "/icon.png";
// ✅ Use path.join
const iconPath = path.join(__dirname, "icon.png");
// ✅ Or use process.resourcesPath for static assets
const iconPath = path.join(process.resourcesPath, "icon.png");
Ready Event Timing
Don't create windows before ready event:
// ❌ Too early
const win = new BrowserWindow(); // Error
app.whenReady().then(() => {
// ✅ Safe to create windows
const win = new BrowserWindow();
});
Also see
- Electron Documentation - Official documentation
- Electron Forge - Recommended build tool
- Electron API Demos - Interactive examples
- Electron Builder - Alternative packaging tool
- Awesome Electron - Curated resources
- Electron Security Checklist - Official security guide