nanoui
Desktop apps in Zig, with native UI drawn on the GPU or a web frontend.
Meet Nano. Transparent PNG · Original paper illustration
Want to see it in use? nanoterm is a terminal with a coding agent, and nanomon an activity monitor, both built on nanoui.
Get started · Native UI · Web pages · Testing · Platforms · Apps · Performance · Credits
Each window shows one of two kinds of interface:
- Native UI, written in Zig alone and drawn by nanoui: no HTML, no JavaScript, no webview. It has flexbox layout, text in a glyph atlas, animations, and views you can test without a window. On macOS it draws with Metal and CoreText, in one instanced draw call per frame.
- A web page in the system webview (WKWebView, macOS for now). The frontend calls Zig through a JavaScript client generated from your Zig types, with TypeScript declarations.
Apps are single binaries. Nothing polls, so an idle app uses no CPU.
Get started
git clone --recursive https://github.com/justrach/nanoui cd nanoui zig build run-gallery # the native UI tour zig build run-hello # the webview example zig build test # unit and widget tests
You need Zig 0.17.0, plus the macOS SDK on a Mac (Xcode or the Command Line Tools). Linux apps need GTK 3 at run time and nothing at build time.
--recursive also fetches the apps, which live in their own repositories as
submodules under apps/. To fetch them after a plain clone, run
git submodule update --init.
Native UI
const Counter = struct { n: i64 = 0, fn view(s: *Counter, c: *ui.Context) void { const col = ui.div(c).column().sizeFull().center().gap(16); defer col.end(); ui.textf(c, .{ .size = 56, .weight = .bold }, "{d}", .{s.n}); if (ui.button(c, "Increment", .{ .variant = .primary }).clicked) s.n += 1; } }; pub fn main(init: std.process.Init) !void { const app = try nanoui.App.create(init, .{ .name = "Counter" }); defer app.destroy(); var state: Counter = .{}; _ = try app.newWindow(.{ .title = "Counter", .content = .{ .view = .init(&state, Counter.view) } }); try app.run(); }
The gallery example, zig build run-gallery.
Views are immediate mode: a function of your state, run on every event. Widgets answer from the previous frame's layout. When an interaction changes state, the frame is built again before it's drawn, so the screen never lags the state.
What's in the toolkit
- Layout: rows, columns, cards and scroll views, with flexbox grow, shrink, absolute placement, and maximum sizes.
- Elements: GPUI-style method chains, such as
ui.div(c).row().gap(8).bg(...). - Lists: a virtualized list (
ui.list) that builds only the rows in view. - Text: wrapping, monospace, weights and rich spans, with text that can be selected and copied like a terminal's.
- Controls: buttons, checkboxes, toggles, sliders, progress bars, segmented controls, badges, and text fields with IME support.
- Images: pictures decoded once and packed into an atlas.
- Look: light and dark themes that follow the system, eased animations, and Gaussian shadows.
- Pointer: hover that follows ancestry as CSS's
:hoverdoes, double clicks, and pointer tracking for graphs (c.wants_moves).
Web pages
const Greeter = struct { pub const nanoui_params = .{ .greet = .{"name"} }; pub fn greet(_: *Greeter, call: *nanoui.Call, name: []const u8) ![]const u8 { return std.fmt.allocPrint(call.arena, "Hello, {s}!", .{name}); } }; const Tick = nanoui.Event("tick", struct { uptime: u64 }); // in main: try app.bind(&greeter); try app.registerEvent(Tick); _ = try app.newWindow(.{ .title = "Hello", .content = .{ .page = .{ .url = "/", .assets = &assets } } });
import { Greeter, onTick } from "/nanoui.js"; // generated, served by the app document.body.textContent = await Greeter.greet("Ada"); onTick(({ uptime }) => console.log(uptime));
zig build gen writes nanoui.d.ts:
export declare const Greeter: { greet(name: string): Promise<string> }; export declare function onTick(listener: (payload: TickPayload) => void): () => void;
Methods may return error unions; an error rejects the page's promise with
the error's name. Assets are served from the nanoui://localhost/ scheme.
Elements styled --app-region: drag move frameless windows.
Testing without a window
var t: ui.Tester = undefined; try t.init(gpa, .init(&state, Counter.view), .{}); defer t.deinit(); try t.clickText("Increment"); try std.testing.expect(t.hasText("1")); try t.screenshot("/tmp/counter.png"); // rendered offscreen by the Metal renderer
The real apps run headless too:
zig-out/bin/gallery --nanoui-snapshot=shot.png --nanoui-script="click:Tasks;type:Hi;key:enter" zig-out/bin/hello --nanoui-eval='nanoui.call("Greeter.greet","Ada").then(v => { nanoui.log(v); nanoui.window.close() })'
Platforms
nanoui loads what each platform has instead of linking it, so every platform
builds from any machine. On a Mac, zig build -Dtarget=x86_64-linux-gnu or
-Dtarget=x86_64-windows-gnu makes a Linux or Windows binary.
| macOS | Linux | Windows | |
|---|---|---|---|
| windows, input, timers | AppKit | GTK 3 (loaded with dlopen) |
Win32 |
| text | CoreText | nanoui's TrueType reader (src/ui/tt/) |
the same |
| rendering | Metal | CPU (src/ui/raster.zig), shown with cairo |
CPU, shown with GDI |
| icons | SF Symbols | line icons drawn by nanoui (src/ui/icons.zig) |
the same |
| pages (webview) | WKWebView | not yet | not yet |
| menus | the menu bar | accelerators | accelerators |
The CPU renderer matches the Metal one pixel for pixel, and a test compares
them. zig build test -Dportable=true runs the Linux and Windows text and
renderer on a Mac. Linux is also tested in a container
(tools/linux.Dockerfile: GTK, fonts and a virtual display).
Apps
nanoterm: a terminal of blocks, in the manner of Warp, with a coding agent (graff or Claude Code) over ACP. It has native visuals, a ⌘P launcher, and SSH sessions where the agent works on the remote host. |
nanomon: an activity monitor in Codegraff colors, with cell graphs of CPU, memory, GPU, disk and network. You can quit several processes at once, and a graff bubble suggests what to quit. |
Download them for macOS: nanoterm · nanomon.
Both live in apps/ as submodules, and each also builds on its own, fetching
nanoui pinned by commit. To build one against the nanoui next to it, with
your changes, use --fork: cd apps/nanomon && zig build run --fork=../...
Performance
Performance is measured headless with hillclimb/:
the same scenes rendered through each framework's own frames and offscreen
Metal renderer, on an Apple M3 Ultra. The figures are medians per frame in
milliseconds; lower is better.
| scene | frame CPU nanoui | frame CPU mygo | GPU nanoui | GPU mygo |
|---|---|---|---|---|
| boxes (600 shadowed boxes) | 0.061 | 0.301 | 0.140 | 1.616 |
| text (300 changing labels) | 0.108 | 0.418 | 0.065 | 0.056 |
| scroll (1000-row column) | 0.315 | 2.076 | 0.055 | 0.122 |
| list (100 000-row virtual list) | 0.060 | 0.367 | 0.046 | 0.045 |
| dashboard (12 cards) | 0.015 | 0.087 | 0.098 | 0.295 |
Release binaries are about 0.7 MB for nanoui and 11.8 MB for mygo. IPC (5000 sequential calls from the page to native code) took 493 ms with nanoui and 528 ms with mygo.
Development
zig build lines # the 600-line limit, enforced zig build test-bin -Dtarget=aarch64-linux-gnu --prefix zig-out/linux # Linux tests, for the container cd hillclimb && zig build run -- --label "change" # benchmarks against mygo and history
Rules for working here, by hand or with an agent, are in AGENTS.md.
nanoui is a subset of what full frameworks offer. Pages work only on macOS so far, and there's no accessibility, bundling, notifications, auto-updates, channels or plugins yet. Bound methods run on the main thread.
License
nanoui is licensed under the MIT License. Material from mygo (MIT) and GPUI (Apache 2.0) keeps its own notices; see third-party notices.
Credits
nanoui stands on the shoulders of two projects:
- mygo by egoist:
nanoui began as a port of its core to Zig, and keeps its architecture. That
means:
- the Objective-C runtime called directly, with runtime classes for delegates;
- a batched outbox with one evaluation per main-loop turn;
- events held until the page is ready;
- a generated, typed client;
- native UI drawn by the framework.
- GPUI by
Zed Industries. From GPUI, nanoui takes:
- elements styled by method chains;
- the virtualized
uniform_list; - layouts cached across frames, with glyphs rasterized once into an atlas;
- Gaussian shadows;
- its general idea of a fast, GPU-drawn native UI.
Built in Zig 0.17 · AGENTS.md · benchmarks · MIT · third-party notices
