Desktop app development guide for cross-platform desktop applications. Covers Electron and Tauri frameworks. Triggers: desktop app, Electron, Tauri, mac app, windows app, 데스크톱 앱, デスクトップアプリ, 桌面应用
/plugin marketplace add popup-studio-ai/bkit-claude-code/plugin install bkit@bkit-marketplaceThis skill is limited to using the following tools:
A guide for developing desktop apps using web technologies (HTML, CSS, JavaScript). Support Windows, macOS, and Linux simultaneously with a single codebase.
| Framework | Recommended For | Advantages | Disadvantages |
|---|---|---|---|
| Electron | Fast development, rich ecosystem | Easy start, many examples | Large bundle size (~150MB) |
| Tauri | Lightweight apps, performance focus | Small bundle (~10MB), fast execution | Requires Rust, smaller ecosystem |
Starter → Electron + electron-vite
- Can start immediately with just web knowledge
Dynamic → Electron + auto-update
- Includes server integration, auto-update
Enterprise → Tauri
- Apps where performance and security matter
# Create with electron-vite (recommended)
npm create @electron-vite/create my-electron-app
cd my-electron-app
# Install dependencies
npm install
# Start development server
npm run dev
my-electron-app/
├── src/
│ ├── main/ # Main process (Node.js)
│ │ └── index.ts # App entry point, window management
│ ├── preload/ # Preload script
│ │ └── index.ts # Renderer↔Main bridge
│ └── renderer/ # Renderer process (Web)
│ ├── src/ # React/Vue code
│ └── index.html # HTML entry point
├── resources/ # App icons, assets
├── electron.vite.config.ts # Build configuration
├── electron-builder.yml # Deployment configuration
└── package.json
┌─────────────────────────────────────────────────────┐
│ Electron App │
├─────────────────────────────────────────────────────┤
│ Main Process (Node.js) │
│ - System API access (files, network, etc.) │
│ - Window creation/management │
│ - Menu, tray management │
├─────────────────────────────────────────────────────┤
│ Preload Script (Bridge) │
│ - Safe main↔renderer communication │
│ - Expose only specific APIs │
├─────────────────────────────────────────────────────┤
│ Renderer Process (Chromium) │
│ - Web UI (React, Vue, etc.) │
│ - DOM access │
│ - No direct Node.js API access (security) │
└─────────────────────────────────────────────────────┘
// src/main/index.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'path';
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
},
});
// Dev mode: Load Vite server
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
// Production: Load built files
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// IPC: Handle requests from renderer
ipcMain.handle('read-file', async (event, filePath) => {
const fs = await import('fs/promises');
return fs.readFile(filePath, 'utf-8');
});
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';
// APIs to safely expose to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Read file
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
// Save file dialog
saveFile: (content: string) => ipcRenderer.invoke('save-file', content),
// App version
getVersion: () => process.env.npm_package_version,
// Platform
platform: process.platform,
});
// Type definitions (for use in renderer)
declare global {
interface Window {
electronAPI: {
readFile: (path: string) => Promise<string>;
saveFile: (content: string) => Promise<void>;
getVersion: () => string;
platform: NodeJS.Platform;
};
}
}
// src/renderer/src/App.tsx
import { useState } from 'react';
function App() {
const [content, setContent] = useState('');
const handleOpenFile = async () => {
const result = await window.electronAPI.readFile('/path/to/file.txt');
setContent(result);
};
return (
<div className="app">
<h1>My Electron App</h1>
<p>Platform: {window.electronAPI.platform}</p>
<button onClick={handleOpenFile}>Open File</button>
<pre>{content}</pre>
</div>
);
}
export default App;
// src/main/menu.ts
import { Menu, app, shell } from 'electron';
const template: Electron.MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{ label: 'New File', accelerator: 'CmdOrCtrl+N', click: () => {} },
{ label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => {} },
{ type: 'separator' },
{ label: 'Quit', role: 'quit' },
],
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', role: 'undo' },
{ label: 'Redo', role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', role: 'cut' },
{ label: 'Copy', role: 'copy' },
{ label: 'Paste', role: 'paste' },
],
},
{
label: 'Help',
submenu: [
{
label: 'Documentation',
click: () => shell.openExternal('https://docs.example.com'),
},
],
},
];
// Add macOS app menu
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
});
}
export const menu = Menu.buildFromTemplate(template);
// src/main/tray.ts
import { Tray, Menu, nativeImage } from 'electron';
import { join } from 'path';
let tray: Tray | null = null;
export function createTray() {
const icon = nativeImage.createFromPath(join(__dirname, '../../resources/icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
const contextMenu = Menu.buildFromTemplate([
{ label: 'Open', click: () => {} },
{ type: 'separator' },
{ label: 'Quit', role: 'quit' },
]);
tray.setToolTip('My App');
tray.setContextMenu(contextMenu);
}
# Prerequisite: Rust installation required
# Install from https://rustup.rs
# Create Tauri project
npm create tauri-app my-tauri-app
cd my-tauri-app
# Install dependencies
npm install
# Start development server
npm run tauri dev
my-tauri-app/
├── src/ # Frontend (React, Vue, etc.)
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/ # Tauri backend (Rust)
│ ├── src/
│ │ ├── main.rs # Main entry point
│ │ └── lib.rs # Command definitions
│ ├── tauri.conf.json # Tauri configuration
│ └── Cargo.toml # Rust dependencies
├── public/
└── package.json
// src-tauri/src/lib.rs
use tauri::command;
#[command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[command]
async fn read_file(path: &str) -> Result<String, String> {
std::fs::read_to_string(path)
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
function App() {
const [greeting, setGreeting] = useState('');
const handleGreet = async () => {
const result = await invoke('greet', { name: 'World' });
setGreeting(result as string);
};
const handleReadFile = async () => {
try {
const content = await invoke('read_file', { path: '/path/to/file.txt' });
console.log(content);
} catch (error) {
console.error(error);
}
};
return (
<div>
<button onClick={handleGreet}>Greet</button>
<p>{greeting}</p>
</div>
);
}
// Web: Not possible (user must select directly)
// Desktop: Free access
// Electron
const fs = require('fs');
fs.writeFileSync('/path/to/file.txt', 'content');
// Tauri
await invoke('write_file', { path: '/path/to/file.txt', content: 'content' });
Things impossible on web but possible on desktop:
- System tray icon
- Global shortcuts
- Native notifications
- Drag and drop (file path access)
- Full clipboard control
- Native menus
Web: Requires Service Worker, limited
Desktop: Works offline by default
⚠️ Server integration features must handle offline!
# electron-builder.yml
appId: com.example.myapp
productName: My App
directories:
buildResources: resources
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.*'
mac:
artifactName: ${name}-${version}-${arch}.${ext}
target:
- dmg
- zip
icon: resources/icon.icns
win:
artifactName: ${name}-${version}-${arch}.${ext}
target:
- nsis
icon: resources/icon.ico
linux:
target:
- AppImage
- deb
# Execute build
npm run build:mac
npm run build:win
npm run build:linux
// src/main/updater.ts
import { autoUpdater } from 'electron-updater';
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', () => {
// Notify update available
});
autoUpdater.on('update-downloaded', () => {
// Restart to apply update
autoUpdater.quitAndInstall();
});
# Build for current platform
npm run tauri build
# Output locations
# macOS: src-tauri/target/release/bundle/dmg/
# Windows: src-tauri/target/release/bundle/msi/
# Linux: src-tauri/target/release/bundle/appimage/
□ Decide local data storage method (SQLite, JSON file, etc.)
□ Decide if cloud sync is needed
□ Consider platform-specific UI guidelines (macOS, Windows)
□ Plan keyboard shortcuts
□ Design menu structure
□ Support dark/light mode
□ Handle window resizing
□ Handle platform-specific UI differences (window control positions, etc.)
□ Don't expose Node.js APIs directly (use contextBridge)
□ Security handling when loading external URLs
□ Encrypt sensitive data storage
□ Code signing (macOS Notarization, Windows Signing)
□ Set up auto-update
□ App Store submission (if needed)
| Library | Purpose |
|---|---|
| electron-store | Local settings/data storage |
| electron-updater | Auto-update |
| electron-log | Logging |
| better-sqlite3 | SQLite database |
| Library | Purpose |
|---|---|
| tauri-plugin-store | Settings storage |
| tauri-plugin-sql | SQLite support |
| tauri-plugin-log | Logging |
| tauri-plugin-updater | Auto-update |
"Set up a [app description] app project with Electron + React.
- Use electron-vite
- Support system tray
- Set up auto-update"
"Implement file open/save functionality.
- Use native file dialogs
- Save recent file list
- Support drag and drop"
"Create electron-builder configuration.
- macOS: DMG + notarization
- Windows: NSIS installer
- Auto-update server integration"
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.