Claude-skill-registry electron-desktop
Desktop application development with Electron for Windows, macOS, and Linux. Use when building cross-platform desktop apps, implementing native OS features, or packaging web apps for desktop.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/electron-desktop" ~/.claude/skills/majiayu000-claude-skill-registry-electron-desktop && rm -rf "$T"
manifest:
skills/data/electron-desktop/SKILL.mdsource content
Electron Desktop Development
Build cross-platform desktop applications using web technologies.
Platforms Supported
| Platform | Architecture | Notes |
|---|---|---|
| Windows | x64, arm64, ia32 | Windows 10+ |
| macOS | x64, arm64 (Apple Silicon) | macOS 10.15+ |
| Linux | x64, arm64, armv7l | Most distributions |
Project Structure
my-app/ ├── src/ │ ├── main/ │ │ ├── main.ts # Main process │ │ ├── preload.ts # Preload scripts │ │ └── ipc.ts # IPC handlers │ ├── renderer/ │ │ ├── index.html │ │ ├── App.tsx │ │ └── components/ │ └── shared/ │ └── types.ts ├── resources/ │ ├── icon.icns # macOS │ ├── icon.ico # Windows │ └── icon.png # Linux ├── electron-builder.yml ├── package.json └── forge.config.ts
Main Process
Entry Point
// src/main/main.ts import { app, BrowserWindow, ipcMain } from "electron"; import path from "path"; let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, titleBarStyle: "hiddenInset", // macOS frame: process.platform === "darwin", // Windows/Linux custom frame show: false, // Show when ready }); // Load the app if (process.env.NODE_ENV === "development") { mainWindow.loadURL("http://localhost:5173"); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, "../renderer/index.html")); } // Show when ready to prevent flash mainWindow.once("ready-to-show", () => { mainWindow?.show(); }); mainWindow.on("closed", () => { mainWindow = null; }); } app.whenReady().then(createWindow); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });
Preload Script (Security Bridge)
// src/main/preload.ts import { contextBridge, ipcRenderer } from "electron"; // Expose safe APIs to renderer contextBridge.exposeInMainWorld("electronAPI", { // File operations openFile: () => ipcRenderer.invoke("dialog:openFile"), saveFile: (content: string) => ipcRenderer.invoke("dialog:saveFile", content), // App info getVersion: () => ipcRenderer.invoke("app:getVersion"), // Window controls minimize: () => ipcRenderer.send("window:minimize"), maximize: () => ipcRenderer.send("window:maximize"), close: () => ipcRenderer.send("window:close"), // Two-way communication onUpdateAvailable: (callback: () => void) => { ipcRenderer.on("update:available", callback); return () => ipcRenderer.removeListener("update:available", callback); }, }); // Type definitions for renderer declare global { interface Window { electronAPI: { openFile: () => Promise<string | null>; saveFile: (content: string) => Promise<boolean>; getVersion: () => Promise<string>; minimize: () => void; maximize: () => void; close: () => void; onUpdateAvailable: (callback: () => void) => () => void; }; } }
IPC Handlers
// src/main/ipc.ts import { ipcMain, dialog, BrowserWindow } from "electron"; import fs from "fs/promises"; export function setupIPC() { // File dialogs ipcMain.handle("dialog:openFile", async () => { const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ["openFile"], filters: [ { name: "Text Files", extensions: ["txt", "md"] }, { name: "All Files", extensions: ["*"] }, ], }); if (canceled || filePaths.length === 0) return null; return fs.readFile(filePaths[0], "utf-8"); }); ipcMain.handle("dialog:saveFile", async (_, content: string) => { const { canceled, filePath } = await dialog.showSaveDialog({ filters: [{ name: "Text Files", extensions: ["txt"] }], }); if (canceled || !filePath) return false; await fs.writeFile(filePath, content); return true; }); // Window controls ipcMain.on("window:minimize", (event) => { BrowserWindow.fromWebContents(event.sender)?.minimize(); }); ipcMain.on("window:maximize", (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win?.isMaximized()) { win.unmaximize(); } else { win?.maximize(); } }); ipcMain.on("window:close", (event) => { BrowserWindow.fromWebContents(event.sender)?.close(); }); }
Renderer Process (React)
Using Exposed APIs
// src/renderer/App.tsx import { useState, useEffect } from "react"; function App() { const [version, setVersion] = useState(""); const [content, setContent] = useState(""); useEffect(() => { window.electronAPI.getVersion().then(setVersion); const unsubscribe = window.electronAPI.onUpdateAvailable(() => { alert("Update available!"); }); return unsubscribe; }, []); const handleOpen = async () => { const fileContent = await window.electronAPI.openFile(); if (fileContent) { setContent(fileContent); } }; const handleSave = async () => { await window.electronAPI.saveFile(content); }; return ( <div className="app"> <header className="titlebar"> <span>My App v{version}</span> <div className="window-controls"> <button onClick={window.electronAPI.minimize}>−</button> <button onClick={window.electronAPI.maximize}>□</button> <button onClick={window.electronAPI.close}>×</button> </div> </header> <main> <button onClick={handleOpen}>Open File</button> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> <button onClick={handleSave}>Save File</button> </main> </div> ); }
Custom Title Bar CSS
.titlebar { -webkit-app-region: drag; /* Make draggable */ height: 32px; display: flex; justify-content: space-between; align-items: center; padding: 0 16px; background: #1e1e1e; color: white; } .window-controls { -webkit-app-region: no-drag; /* Buttons clickable */ display: flex; gap: 8px; } .window-controls button { width: 32px; height: 32px; border: none; background: transparent; color: white; cursor: pointer; } .window-controls button:hover { background: rgba(255, 255, 255, 0.1); }
Native Features
System Tray
import { Tray, Menu, nativeImage } from "electron"; let tray: Tray | null = null; function createTray() { const icon = nativeImage.createFromPath("resources/tray-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 App"); tray.setContextMenu(contextMenu); tray.on("click", () => { mainWindow?.show(); }); }
Native Menus
import { Menu } from "electron"; const template: Electron.MenuItemConstructorOptions[] = [ { label: "File", submenu: [ { label: "Open", accelerator: "CmdOrCtrl+O", click: () => { /* handle */ }, }, { label: "Save", accelerator: "CmdOrCtrl+S", click: () => { /* handle */ }, }, { type: "separator" }, { role: "quit" }, ], }, { label: "Edit", submenu: [ { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, ], }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template));
Notifications
import { Notification } from "electron"; new Notification({ title: "Update Available", body: "A new version is ready to install.", icon: "resources/icon.png", }).show();
Auto Updates
import { autoUpdater } from "electron-updater"; autoUpdater.checkForUpdatesAndNotify(); autoUpdater.on("update-available", () => { mainWindow?.webContents.send("update:available"); }); autoUpdater.on("update-downloaded", () => { autoUpdater.quitAndInstall(); });
Building & Distribution
electron-builder Configuration
# electron-builder.yml appId: com.mycompany.myapp productName: My App copyright: Copyright © 2025 directories: output: dist buildResources: resources files: - dist/**/* - package.json mac: category: public.app-category.developer-tools target: - target: dmg arch: [x64, arm64] - target: zip arch: [x64, arm64] hardenedRuntime: true gatekeeperAssess: false entitlements: build/entitlements.mac.plist notarize: true win: target: - target: nsis arch: [x64] sign: true linux: target: - target: AppImage - target: deb - target: rpm category: Development publish: provider: github releaseType: release
Build Commands
# Development npm run dev # Build for current platform npm run build # Build for all platforms npm run build:all # Build for specific platform npm run build:mac npm run build:win npm run build:linux
Security Best Practices
DO:
- Always use
contextIsolation: true - Use preload scripts for IPC
- Validate all IPC inputs
- Enable
sandbox: true - Sign and notarize for distribution
DON'T:
- Enable
nodeIntegration - Use
moduleremote - Load untrusted content
- Expose full Node.js APIs
- Skip code signing
Alternatives to Consider
| Framework | Best For |
|---|---|
| Tauri | Smaller bundle, Rust backend |
| Neutralino | Lightweight, system webview |
| Electron | Full Node.js, mature ecosystem |