Hello World (Zig)
This guide creates a minimal Electrobun app with a Zig main process. It
mirrors the TypeScript walkthrough — same
project shape, same webview, but the window and webview are created from Zig
instead of Cottontail. The faster path is
hutch electrobun init my-app --template=zig-wgpu.
1. Create the project
mkdir hello-zigcd hello-zigCreate hutch.config.ts. Hutch resolves scripts only from this file; this
project does not need a package.json:
export default { electrobun: { version: "2.0.1-beta.0" }, scripts: { dev: ["hutch", "electrobun", "dev", "--watch"], build: ["hutch", "electrobun", "build", "--env=stable"], },};Create tsconfig.json so editors and TypeScript resolve the config types and
view SDK projected during sync:
{ "extends": "./.hutch/devkit/tsconfig.json"}2. Add a webview
Create src/mainview/index.html:
<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title>Hello Electrobun</title> </head> <body> <h1>Hello from Electrobun</h1> </body></html>3. Add the main process
Create src/zig/main.zig:
const std = @import("std");const electrobun = @import("electrobun");
fn sleepMs(ms: u64) void { electrobun.defaultIo().sleep(.fromMilliseconds(@intCast(ms)), .awake) catch {};}
fn createUi(core: *electrobun.Core, bundle_paths: *const electrobun.BundlePaths) void { // Give the native event loop a beat to start before creating UI. sleepMs(150);
// Required before any createWebview: loads the bundled preload scripts. core.configureWebviewRuntimeFromExecutableDir(bundle_paths, 0) catch |err| { std.debug.print("failed to configure webview runtime: {s}\n", .{@errorName(err)}); return; };
const window_id = core.createWindow(.{ .title = "Hello Electrobun", .frame = .{ .x = 160, .y = 100, .width = 800, .height = 600 }, }) catch |err| { std.debug.print("failed to create window: {s}\n", .{@errorName(err)}); return; };
_ = core.createWebview(.{ .window_id = window_id, .renderer = .native, .url = "views://mainview/index.html", .frame = .{ .x = 0, .y = 0, .width = 800, .height = 600 }, .callbacks = .{ .decide_navigation = electrobun.allowAllNavigation, .event = electrobun.noopWebviewEvent, .event_bridge = electrobun.noopWebviewPostMessage, }, .sandbox = false, }) catch |err| { std.debug.print("failed to create webview: {s}\n", .{@errorName(err)}); core.closeWindow(window_id) catch {}; };}
pub fn main() !void { const allocator = std.heap.c_allocator;
var core = try electrobun.Core.load(allocator); defer core.close();
var bundle_paths = try electrobun.resolveBundlePaths(allocator); defer bundle_paths.deinit(allocator);
var owned_app_info = try electrobun.resolveAppInfoFromBundle(allocator, &bundle_paths); defer owned_app_info.deinit(allocator); const app_info = owned_app_info.borrowed();
// runMainThread blocks the main thread, so build UI from a worker thread. const ui_thread = try std.Thread.spawn(.{}, createUi, .{ &core, &bundle_paths }); ui_thread.detach();
try core.runMainThread(app_info);}Create the project-owned build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const electrobun_sdk = b.option( []const u8, "electrobun-sdk", "Absolute path to the Electrobun Zig SDK projected by Hutch", ) orelse @panic("missing -Delectrobun-sdk; build this project through Hutch");
const electrobun = b.createModule(.{ .root_source_file = .{ .cwd_relative = electrobun_sdk }, .target = target, .optimize = optimize, });
const exe = b.addExecutable(.{ .name = "main", .root_module = b.createModule(.{ .root_source_file = b.path("src/zig/main.zig"), .target = target, .optimize = optimize, .link_libc = true, .imports = &.{ .{ .name = "electrobun", .module = electrobun }, }, }), });
b.installArtifact(exe);}The build owns its source root, output name, modules, and linker settings.
Hutch invokes it with
-Delectrobun-sdk=<project>/.hutch/devkit/zig-sdk/electrobun.zig; it never
generates or rewrites build.zig.
4. Configure the build
Create electrobun.config.ts:
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "hello-zig", identifier: "hellozig.example.dev", version: "0.0.1", }, build: { mainProcess: "zig", zig: { version: "0.16.0", }, copy: { "src/mainview/index.html": "views/mainview/index.html", }, },} satisfies ElectrobunConfig;5. Run the app
hutch electrobun synchutch run devHutch builds and launches the app, then rebuilds it when watched source files change. Press Ctrl+C to stop it.
How this differs from TypeScript
The Zig SDK is a thin, allocator-explicit wrapper over the native core, so the main process makes some steps explicit that Cottontail handles for you:
- Dynamic core loading.
electrobun.Core.load(allocator)openslibElectrobunCore(.dylib/.so/ElectrobunCore.dll) from the executable’s directory at runtime and resolves its symbols. Nothing is linked at build time. Almost every API is a method on the returnedCore. - App identity comes from the bundle.
resolveBundlePathscomputes the executable andResources/directories, andresolveAppInfoFromBundleparsesResources/version.jsonfor the app’sidentifier,name, andchannel— identity is read at runtime, not baked in from config at compile time. configureWebviewRuntimeFromExecutableDirbefore webviews. It loads the bundledpreload-full.js/preload-sandboxed.jsfromResources/and hands them to the core. Skip it and your webviews are missing their preload and bridge scripts.runMainThreadblocks. It runs the native platform event loop on the main thread until the app quits, so windows and webviews are created from a spawned worker thread. The template sleeps ~150ms first to let the loop start; the core marshals UI calls back to the main thread internally.- Error handling is Zig-native. Every fallible call returns
error.ElectrobunCoreFailure, with the real message printed to stderr as[electrobun-zig] core error: ...— handle it withcatchas usual. - Some TS APIs have no Zig equivalent. There is no typed RPC layer (the
bridge is raw JSON via
sendHostMessageToWebviewandpopNextQueuedHostMessage), no updater, and no typed menu builders. See Native main process for the full comparison.
To start from a working Zig project instead of an empty folder:
hutch electrobun init my-app --template=zig-wgpuToolchain
Electrobun 2.0.0 defaults to Zig 0.16.0. Override it exactly with
build.zig.version. Hutch uses a system Zig only on an exact version match;
otherwise it downloads the selected toolchain into
~/.hutch/toolchains/zig/<version>/<platform> and reuses it across projects. The
compiler is not bundled into the finished app.
Native main processes build for the host OS/arch only — there is no cross-compilation, so use native CI runners for a release matrix. Windows targets are x64.
Next: Native main process for the runtime comparison and deeper build details, and the main process APIs reference.