Back to Home

Electron Guide - Cross-Platform Desktop Apps

Building Desktop UIs

Your renderer is just a web page. But desktop apps feel different from websites—and users notice when you get it wrong.

This chapter covers the patterns that make Electron apps feel native: custom title bars, keyboard shortcuts, drag-and-drop, offline support, and responsive layouts that adapt to window resizing.

The tech stack is familiar (React, TypeScript, Tailwind), but the UX patterns are different from web apps.

Why React + TypeScript Works

You don't need React for Electron—vanilla JS, Vue, Svelte all work. But React + TypeScript is the most common stack because it solves real problems: type safety across IPC boundaries, component reusability, and a massive ecosystem of UI libraries.

The challenge: you can't just use Create React App's defaults. You need custom webpack/vite config, TypeScript definitions for Electron APIs, and a build process that handles both main and renderer processes.

🎯 VibeBlaster's Stack Decision

I chose React because the scheduler UI needed complex state management (multiple timers, queue reordering, background sync). TypeScript caught 90% of IPC bugs at compile time—like passing wrong argument types towindow.api.schedulePost().

Tailwind was overkill for the first version, but made rapid iteration possible when redesigning the UI.

Organization Strategy

Separate main and renderer code completely—they run in different processes and have different dependencies. Your renderer should feel like a normal React app that happens to call Electron APIs instead of HTTP endpoints.

Main Process

  • • Group IPC handlers by feature (window, file, auth)
  • • Keep business logic separate from Electron APIs
  • • Use dependency injection for testability

Renderer Process

  • • Standard React patterns (components, hooks, context)
  • • Wrap IPC calls in custom hooks for cleaner code
  • • Share types between main and renderer via .d.ts files

TypeScript: The Key Decision

The biggest TypeScript win in Electron: typed IPC channels. Define your API contract once, get autocomplete and compile-time errors everywhere. This prevents 90% of runtime IPC bugs.

Critical pattern: Create a shared electron.d.tsinterface that both main and renderer import. TypeScript enforces that your preload implementation matches the interface, and your renderer gets full autocomplete.

VibeBlaster has ~30 IPC methods. Before TypeScript, I'd frequently call them with wrong arguments or typo the channel name. After adding types, those bugs disappeared at compile time.

The Type-Safe Pattern

Three steps: define interface → implement in preload → use in React with autocomplete.

// 1. Define once interface ElectronAPI { schedulePost: (content: string, date: Date) => Promise<{id: string}>; } // 2. Implement in preload contextBridge.exposeInMainWorld('api', { schedulePost: (content, date) => ipcRenderer.invoke('schedule', content, date) }); // 3. Use in React with full type safety const {id} = await window.api.schedulePost(content, scheduledDate);

That's it. No complex configuration needed—just import the interface in both processes.

Desktop UX: What Makes It Feel Native

Users expect desktop apps to behave differently from websites. Custom title bars, keyboard shortcuts, window state persistence, drag-and-drop—these aren't optional polish, they're table stakes.

The good news: you already know how to build UI. The challenge is learning the desktop-specific patterns.

🎯 Custom Title Bars: Why Bother?

Native title bars look different on every OS and can't be styled. Custom title bars let you brand your app and control the UX. But you're now responsible for minimize/maximize/close buttons, window dragging, and platform-specific behavior (macOS traffic lights vs Windows controls).

The implementation:

  • • Set frame: false in BrowserWindow to remove OS chrome
  • • Add CSS -webkit-app-region: drag to make your custom title bar draggable
  • • Wire up IPC handlers for minimize/maximize/close buttons
  • • Handle platform differences (macOS traffic light positioning)

VibeBlaster initially used native title bars, but users complained it didn't \"feel like a real app.\" Added custom title bar in v2.0—increased perceived quality significantly.

Desktop UI Patterns That Matter

⌨️Keyboard Shortcuts

Desktop users expect Cmd/Ctrl+Q to quit, Cmd/Ctrl+W to close windows, etc. Implement native menus or handle shortcuts yourself.

Use Electron's Menu API for platform-standard shortcuts automatically.

💾Window State Persistence

Save window size, position, and maximized state. Restore on next launch. Users hate apps that forget their preferences.

VibeBlaster stores this in electron-store, restores in ~50ms on launch.

🎨Native Look & Feel

Match system theme (light/dark mode), use system fonts, respect user's accessibility settings. Electron gives you nativeTheme API.

Auto-switching dark mode is expected, not optional.

🖱️Context Menus

Right-click menus should work everywhere (text inputs get cut/copy/paste, images get save/copy). Build custom or use Electron's Menu.

VibeBlaster uses custom context menus for Twitter-specific actions.

⚠️ Common Mistake: Treating It Like a Website

New Electron devs often build web UIs that happen to run in Electron. The result feels wrong: no keyboard shortcuts, windows always open at default size, no right-click menus, cmd+W doesn't close the window.

Spend time using well-designed desktop apps (Slack, VS Code, Notion). Notice what makes them feel native. Then implement those patterns in your app.

Menus & Shortcuts

Desktop apps need keyboard shortcuts. Users expect Cmd+S to save, Cmd+W to close, Cmd+Q to quit. Electron's Menu API handles this automatically—and gives you platform-appropriate behavior for free.

Native Menus (Recommended)

Use Electron's Menu API to create platform-native menu bars. macOS gets menu bar at top of screen, Windows gets it in the window. Shortcuts work automatically.

Menu.buildFromTemplate([{ label: 'File', submenu: [ { label: 'Save', accelerator: 'CmdOrCtrl+S', click: ... }, { role: 'quit' } ] }])

VibeBlaster uses native menus—less code, better platform integration.

Context Menus

Right-click menus for text inputs, images, and app-specific actions. Build with Menu or custom React components.

• Text inputs: cut/copy/paste/select all

• Images: save/copy image

• Custom: app-specific actions

VibeBlaster adds "Schedule Repost" in tweet context menus.

💡 Pro Tip: Use Roles for Standard Items

Electron menu items support role properties like 'quit', 'copy','paste'. These automatically handle platform differences ("Quit" vs "Exit", menu placement, etc.). Use roles whenever possible instead of implementing manually.

Styling with Tailwind CSS

Tailwind Configuration

// tailwind.config.js
module.exports = {
  content: [
    './src/renderer/**/*.{js,jsx,ts,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        'app-bg': '#1f2937',
        'app-surface': '#374151',
        'app-border': '#4b5563',
      },
      animation: {
        'slide-in': 'slideIn 0.2s ease-out',
      },
      keyframes: {
        slideIn: {
          '0%': { transform: 'translateX(-100%)' },
          '100%': { transform: 'translateX(0)' },
        }
      }
    },
  },
  plugins: [],
};

// src/renderer/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  body {
    @apply bg-gray-900 text-gray-100;
  }
}

@layer components {
  .btn-primary {
    @apply bg-blue-600 hover:bg-blue-700 text-white 
           px-4 py-2 rounded-lg transition-colors;
  }
  
  .card {
    @apply bg-gray-800 border border-gray-700 
           rounded-xl p-6 shadow-lg;
  }
}

Dark Mode Support

// Use nativeTheme to sync with system theme
import { nativeTheme } from 'electron';

ipcMain.handle('get-theme', () => {
  return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
});

ipcMain.on('set-theme', (event, theme: 'dark' | 'light' | 'system') => {
  nativeTheme.themeSource = theme;
});

// Listen for system theme changes
nativeTheme.on('updated', () => {
  mainWindow?.webContents.send('theme-changed', 
    nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
  );
});

UI Development Best Practices

  • Use TypeScript for type safety across IPC boundaries
  • Create custom hooks to encapsulate Electron API calls
  • Match native OS patterns (title bar height, colors, animations)
  • Use CSS variables for theme switching (dark/light mode)
  • Optimize bundle size—tree-shake unused React components
  • Test UI on all target platforms (Windows, macOS, Linux)
  • Handle loading states for async IPC calls gracefully