About electrobun
electrobun is a desktop app framework built with TypeScript, Bun, and the system's native webview. It provides typed, bidirectional RPC between the main process and webview, alongside window, menu, and updater features.
Key features
- 01TypeScript development and bundling with Bun
- 02Use of the system's native webview
- 03Typed, bidirectional RPC between main and webview
- 04Windows, menus, system tray, and file dialogs
- 05App updater and lifecycle events
Use cases
- 01Starting a desktop app from React, Svelte, or other templates
- 02Building a desktop app with multiple windows
- 03Calling between the UI and main process
- 04Implementing menus, tray controls, or file selection
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ gyorkluu/ electrobun-skills --skill electrobunAfter installing, type/electrobunin Claude Code to confirm it loaded.
Details
From the project READMEElectrobun
Electrobun is a TypeScript-first desktop app framework using Bun (runtime + bundler) and the system's native webview. Build apps that are ~14MB, with updates ~14KB, and startup <50ms.
Quick Start
Initialize New Project
bunx electrobun init # Interactive scaffold
bun run dev # Development mode
bun run build # Production bundle
Choose from templates:
- hello-world: Minimal starter
- react-tailwind-vite: React + Tailwind + Vite
- svelte: Svelte framework
- photo-booth: Camera access example
- multitab-browser: Tab-based browser example
Project Structure
my-app/
├── electrobun.config.ts # Build configuration
├── src/
│ ├── bun/
│ │ └── main.ts # Main process (Bun)
│ └── views/
│ └── mainview/
│ ├── index.html
│ └── index.ts # Webview frontend
└── package.json
Core Concepts
Main Process vs Webview
- Main Process (
src/bun/main.ts): Runs in Bun, has full system access, manages windows, handles RPC - Webview Process (
src/views/*/index.ts): Runs in OS webview, sandboxed, handles UI, calls RPC
BrowserWindow
Create and manage application windows.
import { BrowserWindow } from "electrobun/bun";
const win = new BrowserWindow({
title: "My App",
url: "views://mainview/index.html",
width: 1200,
height: 800,
frame: true, // Standard window frame
styleMask: ["titled", "closable", "miniaturizable", "resizable"],
});
Key Options:
title: Window titleurl: Load URL (useviews://protocol for bundled views)width,height: Window dimensionsx,y: Window position (optional)frame: Show standard window frame (true/false)styleMask: Array of window controls (macOS)titleBarStyle: "default" | "hidden" | "hiddenInset"trafficLightPosition: Custom position for macOS traffic lights
Methods:
win.loadURL("views://otherview/index.html")
win.setTitle("New Title")
win.resize({ width: 1024, height: 768 })
win.move({ x: 100, y: 100 })
win.show() / win.hide()
win.close()
win.focus()
win.minimize() / win.maximize() / win.fullscreen()
RPC: Main ↔ Webview Communication
Electrobun's killer feature — typed, fast, bidirectional RPC.
Main Process (bun/main.ts):
import { BrowserWindow } from "electrobun/bun";
const win = new BrowserWindow({ /* ... */ });
// Define what main exposes TO the webview
win.defineRpc({
handlers: {
async getUser(id: string) {
// Full system access here
const user = await db.getUser(id);
return { name: user.name, id: user.id };
},
async saveFile(path: string, content: string) {
await Bun.write(path, content);
return { success: true };
}
}
});
// Call webview methods FROM main
const result = await win.rpc.updateUI({ data: "new data" });
Webview (views/mainview/index.ts):
import { Electroview } from "electrobun/browser";
const electroview = new Electroview();
// Call main process functions
const user = await electroview.rpc.getUser("123");
const result = await electroview.rpc.saveFile("/path/to/file.txt", "content");
// Define handlers main can call
electroview.defineRpc({
handlers: {
async updateUI(data: any) {
// Update DOM here
document.getElementById("content").textContent = data.data;
return { updated: true };
}
}
});
Key Points:
- Fully type-safe (with TypeScript)
- Bidirectional (main can call webview, webview can call main)
- Async by default
- Serialize data automatically (JSON)
Application Menu
import { ApplicationMenu } from "electrobun/bun";
ApplicationMenu.setMenu([
{
label: "File",
submenu: [
{
label: "New Window",
accelerator: "CmdOrCtrl+N",
action: () => createWindow()
},
{ type: "separator" },
{
label: "Quit",
accelerator: "CmdOrCtrl+Q",
action: () => process.exit(0)
},
]
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
]
}
]);
Built-in roles: undo, redo, cut, copy, paste, selectAll, minimize, close, quit
Context Menu
import { ContextMenu } from "electrobun/bun";
win.on("context-menu", (event) => {
ContextMenu.show([
{ label: "Copy", action: () => { /* copy logic */ } },
{ type: "separator" },
{ label: "Paste", action: () => { /* paste logic */ } },
]);
});
System Tray
import { Tray } from "electrobun/bun";
const tray = new Tray({
icon: "assets://tray-icon.png",
tooltip: "My App",
menu: [
{ label: "Show", action: () => win.show() },
{ label: "Hide", action: () => win.hide() },
{ type: "separator" },
{ label: "Quit", action: () => process.exit(0) },
]
});
// Update icon dynamically
tray.setIcon("assets://tray-icon-active.png");
Auto Updater
import { Updater } from "electrobun/bun";
const updater = new Updater({
url: "https://updates.myapp.com/latest.json",
autoCheck: true,
interval: 60 * 60 * 1000, // Check every hour
});
updater.on("update-available", async (info) => {
console.log("Update available:", info.version);
// Show dialog, then:
updater.downloadAndInstall();
});
updater.on("update-downloaded", () => {
console.log("Update ready, restart to apply");
});
updater.on("error", (err) => {
console.error("Updater error:", err);
});
Paths & Assets
import { paths } from "electrobun/bun";
// OS directories
paths.appData // App data directory
paths.userData // User-specific data
paths.resources // Bundled resources
paths.home // User home directory
paths.temp // Temporary directory
// Reference bundled assets:
// views://viewname/file.html → views folder
// assets://file.png → assets folder
Example: Persistent State
import { join } from "path";
const stateFile = join(paths.userData, "state.json");
// Read
const state = JSON.parse(await Bun.file(stateFile).text() || "{}");
// Write
await Bun.write(stateFile, JSON.stringify(state));
Window Events
win.on("close", () => {
console.log("Window closing");
});
win.on("resize", ({ width, height }) => {
console.log("Window resized:", width, height);
});
win.on("move", ({ x, y }) => {
console.log("Window moved:", x, y);
});
win.on("focus", () => console.log("Window focused"));
win.on("blur", () => console.log("Window blurred"));
App Lifecycle Events
import { app } from "electrobun/bun";
app.on("ready", () => {
console.log("App ready, create windows");
});
app.on("before-quit", () => {
console.log("App about to quit, cleanup");
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
Build Configuration
electrobun.config.ts:
import { defineConfig } from "electrobun";
export default defineConfig({
app: {
name: "My App",
version: "1.0.0",
identifier: "com.example.myapp",
},
build: {
main: "src/bun/main.ts",
views: {
mainview: "src/views/mainview/index.ts",
settings: "src/views/settings/index.ts",
},
},
icons: {
mac: "assets/icon.icns",
win: "assets/icon.ico",
linux: "assets/icon.png",
},
updates: {
provider: "generic",
url: "https://updates.myapp.com",
},
});
Common Patterns
Multiple Windows
const windows = new Map<string, BrowserWindow>();
function createWindow(id: string, url: string) {
const win = new BrowserWindow({
url,
width: 800,
height: 600,
});
windows.set(id, win);
win.on("close", () => {
windows.delete(id);
});
return win;
}
Opening External Links
import { shell } from "electrobun/bun";
// In main process
shell.openExternal("https://example.com");
// In webview, intercept link clicks
document.addEventListener("click", (e) => {
const link = (e.target as HTMLElement).closest("a");
if (link && link.href.startsWith("http")) {
e.preventDefault();
electroview.rpc.openExternal(link.href);
}
});
Draggable Title Bar
<!-- In webview HTML -->
<div style="-webkit-app-region: drag; height: 40px; background: #333;">
<h1 style="-webkit-app-region: no-drag;">My App</h1>
<button style="-webkit-app-region: no-drag;">Click Me</button>
</div>
File Dialogs
import { dialog } from "electrobun/bun";
// Open file
const result = await dialog.showOpenDialog({
title: "Select File",
filters: [
{ name: "Images", extensions: ["png", "jpg", "jpeg"] },
{ name: "All Files", extensions: ["*"] }
],
properties: ["openFile", "multiSelections"]
});
if (!result.canceled) {
console.log("Selected files:", result.filePaths);
}
// Save file
const saveResult = await dialog.showSaveDialog({
title: "Save File",
defaultPath: "untitled.txt",
filters: [
{ name: "Text Files", extensions: ["txt"] },
]
});
Platform Notes
macOS
- Uses WKWebView
- Requires code signing for distribution
- Notarization required for Gatekeeper
- Install Xcode Command Line Tools for development
Windows
- Uses WebView2 (Edge)
- Requires Visual Studio Build Tools for development
- Code signing recommended for SmartScreen
Linux
- Uses WebKit2GTK
- Install development packages:
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
Next Steps
- Advanced window management: See
electrobun-window-managementskill for multi-window apps and BrowserView - RPC patterns: See
electrobun-rpc-patternsskill for type safety and performance - Native UI: See
electrobun-native-uiskill for menus, trays, and dialogs - Distribution: See
electrobun-distributionskill for packaging and updates - Debugging: See
electrobun-debuggingskill for troubleshooting
Resources
- Documentation: https://blackboard.sh/electrobun/
- GitHub: https://github.com/blackboardsh/electrobun
- Discord: https://discord.gg/ueKE4tjaCE
- Examples: https://github.com/blackboardsh/electrobun/tree/main/templates