Electron 32: Desktop Apps with JavaScript
AI TOOLS Feb. 9, 2026, 5:30 a.m.

Electron 32: Desktop Apps with JavaScript

Electron has become the go‑to framework for turning web technologies into native‑looking desktop applications. By bundling Chromium and Node.js, it lets you write a single codebase in JavaScript, HTML, and CSS that runs on Windows, macOS, and Linux. In this article we’ll explore Electron 32—its newest LTS release—cover the core concepts you need to get started, and walk through two real‑world examples that you can copy, run, and adapt for your own projects.

Getting Started with Electron 32

Before you dive into code, make sure you have Node.js ≥ 18 installed. Electron ships as an npm package, so you’ll use the familiar npm init workflow to scaffold a project. The first command creates a package.json file that tracks your dependencies and scripts.

Next, install Electron as a development dependency. The --save-dev flag keeps it out of the production bundle, which is useful when you later package the app with tools like electron‑builder.

npm init -y
npm install electron@32 --save-dev

Electron’s entry point is defined by the main field in package.json. By convention we name the file main.js. This script runs in the main process, where you have full access to the operating system, file system, and native APIs.

Project Structure

  • package.json – metadata and scripts.
  • main.js – Electron’s main process code.
  • renderer/ – folder for HTML, CSS, and JavaScript that runs in the Chromium renderer.
  • preload.js – optional bridge that safely exposes Node APIs to the renderer.

With the skeleton in place, you can launch the app using an npm script. Adding "start": "electron ." to the scripts section gives you a one‑liner to spin up the window.

Your First Electron App: “Hello, Electron!”

The classic “Hello, World!” example demonstrates the minimal code required to open a native window and load an HTML page. Create main.js with the following content.

const { app, BrowserWindow } = require('electron');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  win.loadFile('renderer/index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

Now add the renderer HTML file.

<!-- renderer/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello, Electron!</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin-top: 2rem; }
  </style>
</head>
<body>
  <h1>Hello, Electron!</h1>
  <p>Your first desktop app is up and running.</p>
</body>
</html>

Run npm start and you’ll see a native window displaying the greeting. This simple setup already gives you a cross‑platform UI without writing any platform‑specific code.

Pro tip: Keep nodeIntegration disabled and contextIsolation enabled. This prevents untrusted renderer code from accessing Node APIs directly, dramatically reducing the attack surface.

Communicating Between Main and Renderer: IPC

Most desktop apps need to perform background work—reading files, making network requests, or interacting with the OS—and then update the UI. Electron provides an Inter‑Process Communication (IPC) system that lets the renderer send messages to the main process and receive replies.

Let’s extend the “Hello, Electron!” app with a button that reads a local JSON file and displays its content. First, create a preload.js script that safely exposes a custom API to the renderer.

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  readConfig: () => ipcRenderer.invoke('read-config')
});

Modify main.js to handle the IPC call and read the file using Node’s fs module.

// main.js (additional code)
const { ipcMain } = require('electron');
const fs = require('fs');
const path = require('path');

ipcMain.handle('read-config', async () => {
  const configPath = path.join(__dirname, 'config.json');
  const data = await fs.promises.readFile(configPath, 'utf-8');
  return JSON.parse(data);
});

Now update the renderer HTML to include a button and a script that calls the exposed API.

<!-- renderer/index.html (updated) -->
<body>
  <h1>Hello, Electron!</h1>
  <button id="loadBtn">Load Config</button>
  <pre id="output"></pre>

  <script>
    const btn = document.getElementById('loadBtn');
    const out = document.getElementById('output');

    btn.addEventListener('click', async () => {
      try {
        const config = await window.electronAPI.readConfig();
        out.textContent = JSON.stringify(config, null, 2);
      } catch (e) {
        out.textContent = 'Error: ' + e.message;
      }
    });
  </script>
</body>

Finally, add a simple config.json file in the project root.

{
  "appName": "Electron Demo",
  "version": "1.0.0",
  "features": ["IPC", "Packaging", "Cross‑Platform"]
}

When you click “Load Config”, the renderer asks the main process to read the JSON file. The result appears in the <pre> block, demonstrating a clean separation of UI and privileged operations.

Pro tip: Use ipcRenderer.invoke (which returns a promise) instead of the older send/on pattern. It simplifies error handling and keeps the request/response flow intuitive.

Real‑World Use Cases for Electron

Electron powers some of the most popular desktop tools today. Understanding how these apps leverage the framework can inspire your own projects.

  • Visual Studio Code – A full‑featured code editor that uses Electron for its UI while delegating heavy language services to separate processes.
  • Slack – Provides a chat experience with native notifications, file system access, and offline caching, all built on top of Electron.
  • Postman – Offers API testing with a polished UI, integrated request history, and a built‑in console, thanks to Electron’s Chromium rendering.

Common patterns across these apps include:

  1. Custom menus that map to native shortcuts on each OS.
  2. Background workers (Node.js child processes) for long‑running tasks.
  3. Auto‑updating mechanisms that download new releases without user intervention.

When designing your own Electron app, think about which of these patterns align with your product goals. For instance, a note‑taking app might need a system tray icon and offline storage, while a developer tool could benefit from a split‑view layout and hot‑module reloading.

Packaging and Distribution with electron‑builder

After you’ve built a functional prototype, the next step is to ship it. electron‑builder is the most widely used packaging tool; it creates installers for Windows (NSIS), macOS (DMG), and Linux (AppImage, deb, rpm). Install it as a dev dependency and add a build script.

npm install electron-builder --save-dev

Update package.json with a build section that defines the app metadata and target platforms.

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "dist": "electron-builder"
  },
  "build": {
    "appId": "com.codeyaan.myapp",
    "productName": "MyElectronApp",
    "files": [
      "main.js",
      "preload.js",
      "renderer/**",
      "config.json"
    ],
    "mac": {
      "category": "public.app-category.productivity"
    },
    "win": {
      "target": "nsis"
    },
    "linux": {
      "target": "AppImage"
    }
  },
  "devDependencies": {
    "electron": "^32.0.0",
    "electron-builder": "^24.0.0"
  }
}

Run npm run dist and electron‑builder will generate platform‑specific installers in the dist/ folder. The process also signs the binaries (on macOS) and creates auto‑update metadata that you can serve from a static CDN.

Pro tip: Keep your build configuration DRY by extracting common fields (like files) into a separate JSON file and referencing it with the extends property. This makes it easier to maintain multiple product flavors.

Advanced Features: Native Menus and Tray Icons

Desktop users expect native look‑and‑feel for menus, dock behavior, and system‑tray integration. Electron gives you programmatic access to these OS‑level UI components.

Creating a Cross‑Platform Menu

// main.js (menu example)
const { Menu, shell } = require('electron');

const template = [
  {
    label: 'File',
    submenu: [
      { role: 'quit', label: 'Exit' }
    ]
  },
  {
    label: 'Help',
    submenu: [
      {
        label: 'Documentation',
        click: async () => {
          await shell.openExternal('https://www.electronjs.org/docs');
        }
      }
    ]
  }
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

The role property automatically maps to the appropriate native command on each platform (e.g., “Quit” on macOS vs. “Exit” on Windows).

Adding a System Tray Icon

// main.js (tray example)
const { Tray, nativeImage } = require('electron');
let tray = null;

app.whenReady().then(() => {
  const iconPath = path.join(__dirname, 'assets', 'tray.png');
  const trayIcon = nativeImage.createFromPath(iconPath);
  tray = new Tray(trayIcon);

  const contextMenu = Menu.buildFromTemplate([
    { label: 'Show', click: () => win.show() },
    { label: 'Hide', click: () => win.hide() },
    { type: 'separator' },
    { role: 'quit' }
  ]);

  tray.setToolTip('MyElectronApp');
  tray.setContextMenu(contextMenu);
});

With a tray icon, users can hide the main window without quitting the app—a pattern common in chat clients and background utilities.

Pro tip: On macOS, consider using app.dock.hide() and app.dock.show() to control dock visibility based on user actions. This creates a more polished experience for Mac users.

Performance Considerations

Because Electron bundles an entire Chromium instance, memory usage can be higher than native alternatives. However, several strategies keep your app snappy.

  • Lazy‑load heavy UI – Load rarely used windows only when needed, using BrowserWindow’s show: false and loadURL on demand.
  • Use webPreferences wisely – Disable nodeIntegration and enable contextIsolation to reduce the JavaScript context size.
  • Offload CPU‑intensive work – Spawn Node.js worker threads or child processes for tasks like image processing.

Electron 32 introduces a new GPUProcess flag that improves rendering performance on low‑end hardware. Enable it in the app.commandLine.appendSwitch call early in main.js if you target older machines.

Testing Your Electron Application

Automated testing ensures that UI and IPC logic stay reliable as the codebase grows. Two popular tools are spectron (now deprecated) and playwright with Electron support. Below is a minimal Playwright test that launches the app, clicks the “Load Config” button, and asserts the JSON output.

// tests/electron.test.js
const { _electron: electron } = require('playwright');
const path = require('path');

(async () => {
  const app = await electron.launch({ args: [path.join(__dirname, '..')] });
  const window = await app.firstWindow();

  await window.click('#loadBtn');
  const output = await window.textContent('#output');

  console.assert(output.includes('Electron Demo'), 'Config not loaded');
  await app.close();
})();

Run the test with node tests/electron.test.js. Playwright handles the Chromium instance inside Electron, giving you fast, headless CI integration.

Pro tip: Keep UI tests lightweight and focus on critical flows. For business logic, unit‑test the Node modules directly with Jest or Mocha to keep the test suite fast.

Security Best Practices

Security is a first‑class concern in Electron because you’re effectively exposing a full browser to the local file system. Follow these hardening steps:

  1. Enable contextIsolation and disable nodeIntegration in all renderer windows.
  2. Validate and sanitize any data received from the web (e.g., URLs opened via shell.openExternal).
  3. Use a Content‑Security‑Policy header in your HTML to restrict script sources.
  4. Prefer the preload bridge pattern over exposing the entire electron module.

Electron 32 also ships with a built‑in session.setPermissionRequestHandler API that lets you deny or grant permissions (camera, microphone, etc.) on a per‑origin basis.

Conclusion

Electron 32 empowers JavaScript developers to build full‑featured desktop applications without learning a new language or framework. By mastering the main‑renderer split, IPC, native menus, and packaging tools, you can deliver polished, cross

Share this article