Back to Home

Electron Guide - Cross-Platform Desktop Apps

Native Integration

This is where Electron shines. Your app isn't trapped in a browser sandbox—it can read files, show system notifications, live in the tray, and respond to global shortcuts.

Native integration is the reason to choose Electron over a web app. A website can't watch your file system, can't show notifications when minimized, can't register global keyboard shortcuts. Desktop apps can—and users expect them to.

File System: Your App's Superpower

Web apps beg users to upload files through clunky dialogs. Electron apps can watch directories, read configuration files, and auto-save work—just like native apps. The key is doing this securely, through IPC handlers in the main process.

🎯 VibeBlaster Example

VibeBlaster watches a "screenshots" folder. When I drag a screenshot into the folder, the app automatically detects it, uploads it to storage, and makes it available for social media posts. No manual upload step—just drag and forget.

This "magic" is just a file watcher in the main process, sending IPC events to the renderer.

The Three File Patterns

📂File Dialogs

Open/save dialogs feel native because they are native. Electron uses the OS file picker, not a web recreation.

Use: User-initiated file operations

👁️File Watching

Monitor directories for changes. React to new files, modifications, or deletions in real-time.

Use: Auto-sync, hot reload, backup

💾Direct Read/Write

Read config files, write logs, save state. No user interaction needed—just specify the path.

Use: Settings, cache, app data

The Secure Pattern

File operations happen in the main process. The renderer requests operations via IPC, never touching the file system directly. This prevents malicious scripts from accessing arbitrary files.

// Main process: Handle file operations securely
ipcMain.handle('file:open', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Documents', extensions: ['txt', 'md', 'json'] }]
  });
  
  if (result.canceled) return null;
  
  // Read file content and return to renderer
  const content = await fs.readFile(result.filePaths[0], 'utf-8');
  return { path: result.filePaths[0], content };
});

// Renderer: Request file operation through secure bridge
const file = await window.electronAPI.openFile();
if (file) {
  setDocument(file.content);
}

⚠️ Security: Validate All Paths

Never trust file paths from the renderer. Use path.normalize() and verify paths don't escape allowed directories. A malicious script could try ../../../etc/passwd—your IPC handler must reject this.

System Tray: Always Available

The system tray is prime real estate. Your app can live there even when the main window is closed, showing status, providing quick actions, and staying accessible without cluttering the taskbar.

Think of apps like Dropbox, Slack, or Discord—they're "always on" because they live in the tray. Users expect this pattern for background-capable apps.

When to Use Tray

  • • Background sync or monitoring apps
  • • Apps that need to stay running (chat, music)
  • • Quick-access utilities (clipboard managers)
  • • Status indicators (VPN, backup progress)

Tray Capabilities

  • • Custom icon (can change dynamically)
  • • Context menu on right-click
  • • Tooltip on hover
  • • Click to show/hide main window
  • • Balloon notifications (Windows)

Implementation Pattern

Create the tray when the app starts, update it as state changes. On macOS, use template images (grayscale with "Template" suffix) for automatic dark mode support.

// Create tray with context menu
const tray = new Tray(iconPath);
tray.setToolTip('My App - Running');

const contextMenu = Menu.buildFromTemplate([
  { label: 'Show App', click: () => mainWindow.show() },
  { type: 'separator' },
  { label: 'Settings', click: () => openSettings() },
  { label: 'Quit', click: () => app.quit() }
]);

tray.setContextMenu(contextMenu);

// Click to toggle window visibility
tray.on('click', () => {
  mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});

💡 Dynamic Status Updates

Change the tray icon to reflect app state: green for connected, yellow for syncing, red for errors. Update the tooltip with current status. Rebuild the context menu when options change. Users learn to glance at your tray icon for instant status.

Native Notifications

Web notifications are limited and often blocked. Electron notifications use the OS notification system directly—they appear in Notification Center on macOS, Action Center on Windows, and respect user preferences like Do Not Disturb.

🎯 VibeBlaster Example

When a scheduled post goes live, VibeBlaster shows a notification even if the app is minimized. Clicking the notification opens the app and navigates to the post. This keeps users informed without requiring them to watch the app constantly.

Basic Notification

new Notification({
  title: 'Task Complete',
  body: 'Your export finished',
  icon: iconPath
}).show();

With Click Handler

const notif = new Notification({
  title: 'New Message',
  body: 'Click to view'
});

notif.on('click', () => {
  mainWindow.show();
  mainWindow.focus();
});

⚠️ Platform Differences

Windows supports action buttons in notifications; macOS doesn't. macOS notifications persist in Notification Center; Windows notifications can be transient. Always test on each platform and check Notification.isSupported() before showing.

Global Shortcuts

Global shortcuts work even when your app isn't focused. This is essential for utilities like screenshot tools, clipboard managers, or quick-capture apps. The user presses a hotkey anywhere, and your app responds.

Registering Global Shortcuts

import { globalShortcut } from 'electron';

app.whenReady().then(() => {
  // Register global shortcut
  globalShortcut.register('CommandOrControl+Shift+Space', () => {
    // Show/focus your app from anywhere
    if (mainWindow.isVisible()) {
      mainWindow.hide();
    } else {
      mainWindow.show();
      mainWindow.focus();
    }
  });
});

// IMPORTANT: Unregister when app quits
app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});

✓ Good Shortcut Choices

  • • Use modifier combinations (Ctrl+Shift+X)
  • • Let users customize shortcuts
  • • Check for conflicts before registering

✗ Avoid

  • • Common OS shortcuts (Cmd+C, Cmd+V)
  • • Single keys or simple combos
  • • Shortcuts that conflict with popular apps

Platform-Specific Features

Each OS has unique features your app can leverage. The best Electron apps feel native on each platform by using these platform-specific APIs thoughtfully.

🍎 macOS

  • Touch Bar - Custom controls
  • Dock menu - Right-click actions
  • Dock badge - Notification count
  • Recent documents - File menu
  • Handoff - Continuity

🪟 Windows

  • Jump List - Taskbar shortcuts
  • Taskbar progress - Progress bar
  • Overlay icon - Status badge
  • Thumbnail toolbar - Preview buttons
  • Toast actions - Notification buttons

🐧 Linux

  • Desktop files - App launcher
  • Unity launcher - Quick list
  • AppIndicator - System tray
  • MPRIS - Media controls
  • XDG - Standard paths

💡 Pro Tip: Progressive Enhancement

Check process.platform and add platform-specific features as enhancements, not requirements. Your app should work everywhere, but feel extra polished on each platform. VibeBlaster adds Touch Bar controls on macOS and Jump List items on Windows—neither breaks the other platform.

Native Integration Best Practices

All file operations go through main process IPC handlers
Validate and sanitize all file paths from renderer
Use async file operations to avoid blocking
Clean up file watchers when no longer needed
Test notifications on all target platforms
Let users customize global shortcuts
Use platform-specific features as enhancements
Unregister shortcuts and clean up on app quit

Your App Now Has Superpowers

You can read files, watch directories, show notifications, live in the system tray, and respond to global shortcuts. These are the features that make desktop apps feel powerful—and they're all accessible through simple Electron APIs.

Next up: Security. With great power comes great responsibility—we need to make sure these native capabilities can't be exploited by malicious code.