nanoui README

The project's README from github.com/justrach/nanoui. As one markdown file: README.md.

Nano, nanoui's field mouse craftsperson, wearing a blue workshop jacket and holding a miniature app window

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.

macOS · Linux · Windows Apps of about 0.7 MB Zero dependencies Built in Zig 0.17 MIT

Get started · Native UI · Web pages · Testing · Platforms · Apps · Performance · Credits

nanomon, built on nanoui: the overview in Codegraff Light, with cell graphs of CPU, memory, GPU, disk and network

Each window shows one of two kinds of interface:

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.

To use nanoui in your own project, add a release to its build.zig.zon:

zig fetch --save git+https://github.com/justrach/nanoui#v0.2.0

Releases are tagged vMAJOR.MINOR.PATCH, as mygo's are, and each is listed under Releases with what changed. While nanoui is 0.x, a minor release may change the API; a patch release won't.

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 nanoui gallery: buttons, toggles, sliders and a text field, drawn by nanoui
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, Gaussian shadows, and Liquid Glass.
  • Pointer: hover that follows ancestry as CSS's :hover does, double clicks, and pointer tracking for graphs (c.wants_moves).

Liquid Glass

.glass(.{}) fills a box with Liquid Glass, the material of macOS 26 and later, in place of a background. Whatever was painted under the box shows through it: frosted, bent near its edges as through the rim of a lens, and lit along a hairline rim. That includes content scrolling under a toolbar placed over it with absolute().

const bar = ui.div(c).absolute().offset(20, 20).w(420).h(56).row().itemsCenter().px(16).rounded(28).glass(.{});
defer bar.end();
ui.text(c, "On glass", .{ .weight = .semibold });
ui.spacer(c);
// A prominent button: glass tinted with the accent.
const tap = ui.div(c).px(16).py(8).rounded(16).glass(.{ .tint = th.accent }).control();
defer tap.end();
ui.text(c, "Tap", .{ .color = th.accent_text });

Regular glass frosts and lightens what shows through, or darkens it in dark mode, so text on it reads over anything. .style = .clear barely frosts it, for glass over photos and video. nanoui draws the glass itself, after mygo's glass plugin and its measurements of AppKit's NSGlassEffectView. Each pane averages and blurs what's under it, then shades it on the GPU, and the CPU renderer computes the same pixels. It looks the same on Linux and Windows, and a test checks the two renderers agree.

A toolbar of Liquid Glass over colored cards, with a tinted button and a pane of clear glass
The gallery's Glass page.

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: an agent's answer with a native visual

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: the overview with graff's suggestions

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
sh tools/version.sh 0.2.1                                             # set the version for a release

To release, set the version, commit, and push its tag (git tag v0.2.1 && git push origin v0.2.1). The release workflow checks that the tag matches build.zig.zon, runs the tests, and opens the GitHub release.

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, channels or plugins yet. Apps update themselves from GitHub releases on macOS (nanoui.Updater). 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:

Built in Zig 0.17 · AGENTS.md · benchmarks · MIT · third-party notices

nanoui (justrach/nanoui)

Searching Database…

325 ·GitHub
What is this?nanoui is a Zig framework for building desktop apps whose UI is drawn natively on the GPU (no HTML, CSS, or JavaScript) or, optionally, as a web page shown in the system webview.
Is the UI drawn natively or do I write HTML/CSS for it?Native UI is written entirely in Zig with flexbox-style layout and a glyph atlas for text, drawn directly on the GPU with no HTML, JavaScript, or webview involved. You can also show a web page in the system webview instead, with a typed JS client generated from your Zig types.
How do I add nanoui to my project?Add a release of nanoui to your project's build.zig.zon; releases are tagged vMAJOR.MINOR.PATCH and each lists what changed.
What do I need installed before building?You need Zig 0.17.0; on macOS you also need the macOS SDK (Xcode or the Command Line Tools), and on Linux GTK 3 must be present at runtime (but not at build time).
What doesn't work yet?The web-page (webview) mode currently only works on macOS, not Linux or Windows. There's also no accessibility support, app bundling, notifications, channels, or plugin system yet.
Can I build Linux and Windows binaries from a Mac?Yes - nanoui loads platform APIs dynamically rather than linking them, so a Mac can cross-compile Linux or Windows binaries with zig build -Dtarget=x86_64-linux-gnu or -Dtarget=x86_64-windows-gnu.
What example apps come with nanoui?Two example apps, nanoterm and nanomon, live under apps/ as submodules and each builds on its own, pinned to a nanoui commit.
Why build this instead of using Electron or a web framework?Apps compile to single binaries (~0.7 MB release size) that use no CPU when idle, since nothing polls, and native rendering benchmarks several times faster per frame than the author's earlier JS-based framework, mygo.
What is Liquid Glass?Liquid Glass is the frosted, lensed material from macOS 26 that nanoui replicates with .glass(.{}): it blurs and shades whatever is painted beneath a box, rendered identically by both the GPU and CPU renderers on all platforms.
How does nanoui's performance compare to mygo?In headless benchmarks on an Apple M3 Ultra, nanoui's CPU frame times are roughly 3-7x faster than mygo across scenes like scrolling, lists, and dashboards (e.g. 0.315ms vs 2.076ms for a 1000-row scroll).
Creator
justrach
Size
5.8 MB
Stars
325
Age
1d
Commits · 90 Days