Electron Fiddle as a learning tool
Electron Fiddle is a sandbox app written with Electron and maintained by Electron's maintainers. It is recommended as a learning tool to experiment with Electron's APIs or prototype features during development. Fiddle integrates with the documentation and many tutorial examples include an 'Open in Fiddle' button that automatically loads the example into Fiddle without requiring copy-pasting.
Electron documentation structure and categories
The official Electron documentation is organized into the following categories: Tutorial (end-to-end guide for creating and publishing first Electron application), Processes in Electron (in-depth reference on Electron processes), Best Practices (important checklists for developing Electron apps), Examples (quick references to add features), Development (miscellaneous development guides), Distribution (how to distribute app to end users), Testing And Debugging (how to debug JavaScript, write tests, and tools for quality), References (useful links to understand the Electron project), and Contributing (compiling Electron and making contributions).
Accelerators are case-insensitive
Accelerators in Electron are case-insensitive, meaning keyboard shortcuts defined with uppercase or lowercase characters are treated the same way.
Available modifier keys in accelerators
The available modifier keys for accelerators are: Command (or Cmd), Control (or Ctrl), CommandOrControl (or CmdOrCtrl), Alt, Option, AltGr, Shift, and Super (or Meta as alias).
Available key codes in accelerators
Available key codes include: digits 0-9, letters A-Z, function keys F1-F24, punctuation marks (), !, @, #, $, %, ^, &, *, (, :, ;, +, =, <, ,, _, -, >, ., ?, /, ~, `, {, ], [, |, \, }, ", Plus, Space, Tab, Capslock, Numlock, Scrolllock, Backspace, Delete, Insert, Return (or Enter), arrow keys (Up, Down, Left, Right), Home, End, PageUp, PageDown, Escape (or Esc), media keys (VolumeUp, VolumeDown, VolumeMute, MediaNextTrack, MediaPreviousTrack, MediaStop, MediaPlayPause), PrintScreen, and NumPad keys (num0-num9, numdec, numadd, numsub, nummult, numdiv).
Use CommandOrControl instead of Command for cross-platform support
On Linux and Windows, the Command modifier has no effect. You should use CommandOrControl instead, which represents Command (⌘) on macOS and Control on Linux and Windows.
Local keyboard shortcuts triggered only when app is focused
Local keyboard shortcuts are triggered only when the application is focused. These shortcuts map to specific menu items within the app's main application menu.
Cross-platform modifier mapping
Modifiers map differently across platforms: CommandOrControl maps to Command (⌘) on macOS and Control on Windows/Linux; Command maps to Command (⌘) on macOS only; Control maps to Control (^) on macOS and Control on Windows/Linux; Alt maps to Option (⌥) on macOS and Alt on Windows/Linux; Option maps to Option (⌥) on macOS only; Super/Meta maps to Command (⌘) on macOS and Windows (⊞) on Windows.
Common cross-platform accelerator examples
Common cross-platform Electron accelerators are: Copy (CommandOrControl+C), Paste (CommandOrControl+V), Undo (CommandOrControl+Z), Redo (CommandOrControl+Shift+Z).
Define local shortcuts using MenuItem accelerator property
To define a local keyboard shortcut, configure the accelerator property when creating a MenuItem. The click event associated with that menu item will trigger upon using that accelerator.
Local shortcut example with dialog
Example of defining a local keyboard shortcut:
const { dialog, Menu, MenuItem } = require('electron/main')
const menu = new Menu()
if (process.platform === 'darwin') {
const appMenu = new MenuItem({ role: 'appMenu' })
menu.append(appMenu)
}
const submenu = Menu.buildFromTemplate([{
label: 'Open a Dialog',
click: () => dialog.showMessageBox({ message: 'Hello World!' }),
accelerator: 'CommandOrControl+Alt+R'
}])
menu.append(new MenuItem({ label: 'Custom Menu', submenu }))
Menu.setApplicationMenu(menu)
This example opens a dialog when pressing Cmd+Option+R on macOS or Ctrl+Alt+R on other platforms.
Accelerators work even when menu items are hidden
Accelerators can work even when menu items are hidden. On macOS, this feature can be disabled by setting acceleratorWorksWhenHidden: false when building a MenuItem.
Hide accelerator from system menu without disabling it
On Windows and Linux, the registerAccelerator property of MenuItem can be set to false so that the accelerator is visible in the system menu but not enabled.
Global shortcuts work when app is out of focus
Global keyboard shortcuts work even when the app is out of focus. Use the globalShortcut.register() function to specify global shortcuts.
Register global shortcut example
Example of registering a global keyboard shortcut:
const { dialog, globalShortcut } = require('electron/main')
globalShortcut.register('CommandOrControl+Alt+R', () => {
dialog.showMessageBox({ message: 'Hello World!' })
})
This registers a global shortcut that opens a dialog when pressed, regardless of whether the app is focused.
Unregister global shortcut
To unregister a global shortcut, use the globalShortcut.unregister() function:
const { globalShortcut } = require('electron/main')
globalShortcut.unregister('CommandOrControl+Alt+R')
globalShortcut on macOS has QWERTY layout limitation
On macOS, there is a long-standing bug with globalShortcut that prevents it from working with keyboard layouts other than QWERTY (electron/electron#19747).
Handle keyboard shortcuts in renderer process with keydown/keyup events
To handle keyboard shortcuts within a BaseWindow in the renderer process, listen for keydown and keyup DOM Events using the addEventListener API.
Renderer process keyboard shortcut example
Example of handling keyboard events in the renderer process:
function handleKeyPress (event) {
document.getElementById('last-keypress').innerText = event.key
console.log(`You pressed ${event.key}`)
}
window.addEventListener('keyup', handleKeyPress, true)
The third parameter true indicates that the listener will always receive key presses before other listeners so they cannot have stopPropagation() called on them.
Use Alt instead of Option for cross-platform support
The Option (⌥) key only exists on macOS. Use Alt instead, which will map to the appropriate modifier on all platforms.
Intercept keyboard events in main process with before-input-event
The before-input-event event is emitted before dispatching keydown and keyup events in the renderer process. It can be used to catch and handle custom shortcuts that are not visible in the menu.
Main process keyboard interception example
Example of intercepting keyboard events in the main process:
const { app, BrowserWindow } = require('electron/main')
app.whenReady().then(() => {
const win = new BrowserWindow()
win.loadFile('index.html')
win.webContents.on('before-input-event', (event, input) => {
if (input.control && input.key.toLowerCase() === 'i') {
console.log('Pressed Control+I')
event.preventDefault()
}
})
})
This example intercepts Ctrl+I and prevents the default behavior.
Check for deep link on cold start in Windows/Linux
In the app.whenReady() handler on Windows and Linux, inspect process.argv to check for a deep link URL on cold start. If the last argument starts with the protocol scheme, process it. This is separate from handling the second-instance event.
macOS uses open-url event for deep links
On macOS, when a user clicks a deep link to open the app, the 'open-url' event is emitted on the app object. The URL is passed as the second argument to the event listener. This is different from Windows and Linux which use the 'second-instance' event.
Windows and Linux emit second-instance for deep links
On Windows and Linux, when a user clicks a deep link to open the app, the running instance receives a 'second-instance' event rather than 'open-url'. The deep link URL is passed as the last element in the commandLine array argument. Call app.requestSingleInstanceLock() to prevent multiple instances and handle the second-instance event to focus the window and process the URL.
setAsDefaultProtocolClient registers custom protocol handler
Call app.setAsDefaultProtocolClient(protocol, execPath, args) to register an Electron app as the default handler for a custom protocol. During development with process.defaultApp, pass process.execPath and [path.resolve(process.argv[1])] as arguments. In production, call with just the protocol name.
LanguageModelUtility implementation example
The utility process script must register a LanguageModelUtility subclass. The handler receives a details object with webContentsId and securityOrigin properties. The handler must return a class extending LanguageModelUtility with the following static and instance methods: static async create(options) where options includes signal (AbortSignal) and initialPrompts; static async availability() returning 'available', 'downloadable', 'downloading', or 'unavailable'; async prompt(input) receiving LanguageModelMessage[] and returning a string or ReadableStream; async clone() creating a copy; destroy() for cleanup. The create method should set contextUsage and contextWindow properties.
Local AI Handler quick start code example
Three files are needed. First, ai-handler.js (Utility Process):
```js
const { localAIHandler, LanguageModelUtility } = require('electron/utility')
localAIHandler.setPromptAPIHandler((details) => {
return class MyLanguageModel extends LanguageModelUtility {
static async create (options) {
return new MyLanguageModel({
contextUsage: 0,
contextWindow: 4096
})
}
static async availability () {
return 'available'
}
async prompt (input) {
return 'This is a response from your local LLM!'
}
async clone () {
return new MyLanguageModel({
contextUsage: this.contextUsage,
contextWindow: this.contextWindow
})
}
destroy () {
}
}
})
```
Second, main.js (Main Process):
```js
const { app, BrowserWindow, utilityProcess } = require('electron')
const path = require('node:path')
app.whenReady().then(() => {
const aiHandler = utilityProcess.fork(path.join(__dirname, 'ai-handler.js'))
const win = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'AIPromptAPI'
}
})
win.webContents.session.registerLocalAIHandler(aiHandler)
win.loadFile('index.html')
})
```
Third, index.html (Renderer Process):
```html
<script>
async function askAI () {
const model = await LanguageModel.create()
const response = await model.prompt('What is Electron?')
document.getElementById('response').textContent = response
}
</script>
<button onclick="askAI()">Ask AI</button>
<p id="response"></p>
```
Disconnect AI handler from session
Pass null to ses.registerLocalAIHandler() to disconnect the AI handler from a session. After clearing, any LanguageModel.create() calls from renderers using that session will fail.
Late handler registration and request queueing
If a renderer uses the Prompt API after ses.registerLocalAIHandler() has been called but before localAIHandler.setPromptAPIHandler() has been called in the utility process, the request is not immediately rejected. Instead, Electron queues pending requests. Once setPromptAPIHandler() is called, all queued requests are flushed and handled normally. If too many requests arrive before the handler is set, the oldest pending request is dropped and pending promises will reject in the renderer.
Local AI Handler security considerations
The details object passed to your handler includes webContentsId and securityOrigin. Use these to decide whether to handle a request and when to reuse a model instance versus providing a fresh instance to provide proper isolation between origins.
Local AI Handler architecture overview
The Local AI Handler is an experimental Electron API that lets you route Prompt API calls to a local LLM running in a utility process. It involves three processes: the main process (creates UtilityProcess and registers it via ses.registerLocalAIHandler()), the utility process (runs a script that calls localAIHandler.setPromptAPIHandler() to supply a LanguageModelUtility subclass), and the renderer process (web content uses the LanguageModel API). When a renderer calls the Prompt API, Electron proxies the request through the main process to the registered utility process, which invokes the LanguageModel implementation and sends the result back to the renderer.
Enable Prompt API in BrowserWindow
The Prompt API Blink feature must be enabled on any BrowserWindow that will use it. Set enableBlinkFeatures to 'AIPromptAPI' in the webPreferences. To enable multi-modal inputs, also add 'AIPromptAPIMultimodalInput'.
Safe way to load native modules with Web Workers
The safe way to load a native module when using Web Workers is to ensure the app loads no native modules after the Web Workers get started. This can be enforced by overriding process.dlopen to throw an error.
Example: enable nodeIntegrationInWorker in BrowserWindow
const win = new BrowserWindow({
webPreferences: {
nodeIntegrationInWorker: true
}
})
nodeIntegrationInWorker option enables Node.js in Web Workers
Set nodeIntegrationInWorker to true in webPreferences to run Node.js features in Electron's Web Workers. The nodeIntegrationInWorker option can be used independent of nodeIntegration, but sandbox must not be set to true.
nodeIntegrationInWorker not available in SharedWorker or ServiceWorker
The nodeIntegrationInWorker option is not available in SharedWorkers or ServiceWorkers due to incompatibilities in sandboxing policies.
Node.js built-in modules supported in Web Workers
All built-in modules of Node.js are supported in Web Workers. ASAR archives can still be read with Node.js APIs. However, none of Electron's built-in modules can be used in a multi-threaded environment.
Native Node.js modules not recommended in Web Workers
Any native Node.js module can be loaded directly in Web Workers, but it is strongly recommended not to do so. Most existing native modules have been written assuming a single-threaded environment, and using them in Web Workers will lead to crashes and memory corruptions. Even if a native Node.js module is thread-safe, it is still not safe to load it in a Web Worker because the process.dlopen function is not thread safe.
Example: disable native module loading for Web Worker safety
process.dlopen = () => {
throw new Error('Load native module is not safe')
}
const worker = new Worker('script.js')
Online status detection reliability
Both navigator.onLine and net methods provide a strong indicator when offline (returning false), but a true value does not guarantee successful internet connectivity. False positives can occur in cases such as when the computer is running virtualization software with virtual Ethernet adapters in an 'always connected' state. If you need to determine Internet access status reliably, you should develop additional means for this check.
Checking connection status in main process avoids IPC overhead
If you need to check the connection status in the main process, you can use net.isOnline() directly instead of communicating from the renderer process via IPC, which avoids unnecessary inter-process communication overhead.
navigator.onLine in renderer process
The renderer process can detect online/offline status using the navigator.onLine attribute and online/offline events, which are part of the standard HTML5 API. The navigator.onLine attribute returns false if all network requests are guaranteed to fail (e.g. when disconnected from the network), and true in all other cases.
net.isOnline() and net.online in main process
The main process can detect online/offline status using either the net.isOnline() method or the net.online property. Both return the same boolean value with the same reliability characteristics as navigator.onLine. The net module is only available after the app emits the 'ready' event.
Window online/offline event listeners example
The renderer process can listen for 'online' and 'offline' window events to detect connection status changes. Example: window.addEventListener('online', updateOnlineStatus) and window.addEventListener('offline', updateOnlineStatus). The event handler can then check navigator.onLine to update the UI.
Process-specific type aliases usage example
Example showing how to use process-specific module type aliases:
```js
const { shell } = require('electron/common')
const { app } = require('electron/main')
```
These aliases have no impact on runtime but can be used for typechecking and autocomplete.
Electron inherits multi-process architecture from Chromium
Electron's architecture is inherited from Chromium and is very similar to a modern web browser. It uses a multi-process model where the framework is structurally similar to how browsers organize their processes.
Single process limitation: one crashed website crashes entire browser
In single-process browser architectures, if one website crashes or hangs, it affects the entire browser because all tabs and functionality run in the same process. This was a fundamental problem browsers needed to solve.
Multi-process model isolates each tab in separate process
The Chrome team solved the single-process problem by making each tab render in its own process, limiting the harm that buggy or malicious code on a web page could cause to the app as a whole. A single browser process controls these renderer processes and manages the application lifecycle.
Electron apps control two types of processes: main and renderer
As an Electron app developer, you control two types of processes. The main process is analogous to Chrome's browser process, and renderer processes are analogous to Chrome's renderer processes. Each BrowserWindow creates a separate renderer process.
Main process is single entry point for Electron app
Each Electron app has exactly one main process, which serves as the application's entry point. The main process runs in a Node.js environment, meaning it has the ability to require modules and use all of Node.js APIs.
BrowserWindow module creates application windows in main process
The main process uses the BrowserWindow module to create and manage application windows. Each instance of the BrowserWindow class creates an application window that loads a web page in a separate renderer process.
Access web content via webContents object from main process
You can interact with web content from the main process using the window's webContents object. The webContents object is also accessible for embedded web content created with modules like BrowserView.
BrowserWindow creation example with loadURL
Example code showing how to create a BrowserWindow and load a URL:
```js
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')
const contents = win.webContents
console.log(contents)
```
BrowserWindow is EventEmitter for user events
Because the BrowserWindow module is an EventEmitter, you can add handlers for various user events such as minimizing or maximizing the window.
Destroying BrowserWindow terminates corresponding renderer process
When a BrowserWindow instance is destroyed, its corresponding renderer process is automatically terminated as well.
Main process controls application lifecycle via app module
The main process controls the application's lifecycle through Electron's app module, which provides a large set of events and methods for adding custom application behavior such as programmatically quitting the app, modifying the application dock, or showing an About panel.
Quit app when no windows open on non-macOS platforms example
Example code for application lifecycle management:
```js
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
```
This quits the app when no windows are open on Windows and Linux, but not on macOS where applications typically remain open without windows.
Main process exposes native APIs for desktop functionality
The main process adds custom APIs beyond being a Chromium wrapper to interact with the user's operating system. Electron exposes modules that control native desktop functionality such as menus, dialogs, and tray icons.
Renderer process spawned for each BrowserWindow
Each Electron app spawns a separate renderer process for each open BrowserWindow and for each web embed. A renderer is responsible for rendering web content.