The zenit manual
Build truly native macOS desktop UI in Zig. zenit folds windows, GPU rendering, text, input methods and accessibility into one clear UI model; this manual takes you from your first window to a maintainable production app.
const std = @import("std");
const ui = @import("ui");
const App = @import("zenit_app").App;
fn mountUi(cx: *ui.Cx, _: *ui.Scope) anyerror!*ui.Node {
return ui.text(cx, "Hello, zenit!", .{
.font_size = cx.tokens.font_size.xxxl,
.color = cx.tokens.color.fg_primary,
});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const app = try App.init(gpa.allocator(), .{
.window = .{ .title = "Hello" },
});
defer app.deinit();
try app.runWith(mountUi);
}Learning path
Work through it once, in order (about 45 minutes), and you’ll have the whole mental model for building zenit apps on your own.
The architecture you use
Apps depend only on the stable public layer; zenit owns platform and rendering.
zenit.attach(dep, exe) adds exactly two module imports to your executable (ui and zenit_app), then compiles the native bridges and links the system frameworks. Internal modules aren’t in your build graph at all: not “please don’t”, but “can’t”.More than a set of APIs
These three run through the runtime, the text stack and the tooling. Each is backed by a runnable demo and real-window evidence.
Three principles
Follow them and your code stays clear while zenit is still moving fast.
Use ui.*, ui.<group>.* and zenit_app.App. Never reach into internal directories.
Persistent state lives in Signals or bound state; derive with Memo, side-effect with Effect.
Color, type, spacing and radii come from the theme; when you must go off-scale, say so with ui.arb.