Knots is a cross-platform immediate-mode GUI library for Zig. You write the interface as Zig code, and the same code runs on macOS, Windows, Linux, and in the browser.
The UI engine does not depend on a window system or a graphics API. App is the
bundled window and renderer. ui.Context and render.Packet let you embed
Knots in your own window or renderer.
| Platform | Default backend | Other backends |
|---|---|---|
| macOS | WebGPU | Vulkan (MoltenVK) |
| Linux (Wayland) | Vulkan | WebGPU |
| Windows | Vulkan | WebGPU |
| WASM (freestanding) | WebGPU | — |
Select a backend with the gpu_backend dependency option. Read
GPU backends.
- Linux windows use Wayland only. There is no X11 support.
- Text uses one glyph for each Unicode codepoint. There is no complex shaping, bidirectional text, ligatures, kerning, font fallback, or IME composition.
- Accessibility (AccessKit) is available on macOS, Windows, and Linux. It is not available in the browser.
- Zig. The minimum version is in build.zig.zon. Knots follows the Zig master branch.
- On Linux: the Wayland development packages
wayland-client,wayland-cursor,wayland-protocols,wayland-scanner,pkg-config, andxkbcommon. - For the Vulkan backend: a Vulkan 1.3 driver with dynamic rendering.
- For the browser: a browser with WebGPU.
zig fetch --save git+https://github.com/knots-ui/knots.gitAdd the knots and ui modules to your executable:
const knots = b.dependency("knots", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("knots", knots.module("knots"));
exe.root_module.addImport("ui", knots.module("ui"));Build the UI in a frame callback:
const std = @import("std");
const knots = @import("knots");
const ui = @import("ui");
pub fn main(init: std.process.Init) !void {
var app = try knots.App.init(init.io, init.gpa, .{
.window = .{ .width = 1280, .height = 720, .title = "Knots" },
});
defer app.deinit();
try app.start(frame);
}
fn frame(_: *knots.View, context: *ui.Frame) !void {
const size = context.input().logical_extent;
try context.e(.{
ui.component.Rect{ .key = .src(@src()), .style = &.{
.width = .fixed(@floatFromInt(size.width)),
.height = .fixed(@floatFromInt(size.height)),
.padding = .all(16),
.background = .bg,
} },
.{
ui.component.Text{ .key = .src(@src()), .content = "Hello from Knots" },
},
});
}View contains the owning app, the viewport id, and a snapshot of the
renderer status. To get your own state, put knots.App in a field of your
struct and use @fieldParentPtr("app", view.app). Keep the App at one
address until start returns.
Knots can compile UI modules to WebAssembly and reload them in a running native
or browser host. Use Knots.HMR in build.zig and knots.Modules in the host.
Read Hot reloading. The
playground is a complete HMR host.
Import ui, input, and render. Add renderer when you use the bundled GPU
backend:
app_module.addImport("ui", knots_dependency.module("ui"));
app_module.addImport("input", knots_dependency.module("input"));
app_module.addImport("render", knots_dependency.module("render"));
app_module.addImport("renderer", knots_dependency.module("renderer"));var context = try ui.Context.init(allocator, .{});
defer context.deinit();
var frame = try context.beginFrame(host_input);
defer frame.deinit();
try buildUi(&frame);
const output = try context.endFrame(&frame);
window.setCursorShape(output.cursor_shape);
if (output.clipboard_write) |value| {
_ = try window.setClipboardText(allocator, value);
}
var submission = try gpu_frame.begin();
const prepared = try painter.prepare(&output.packet, &.{
.width = target_width,
.height = target_height,
.content_scale = host_input.content_scale,
.upload_slot = submission.upload_slot,
.frame_context = submission,
.linear_target = false,
});
try painter.encode(&prepared, &host_pass);Use Renderer.render when Knots owns the surface. Use Painter.prepare and
Painter.encode when the host owns render passes, submission, or presentation.
render.Packet does not depend on a graphics API, but its geometry, glyph,
clip, and shader conventions are the Knots protocol. A renderer for another API
reads the packet directly. Painter uses the bundled backend types. See
examples/embedded and
Embedding.
- Packet data, input slices, image bytes, and callback data are borrowed. Use them before the next frame, or copy them for async work.
- The host must keep textures and callback resources alive until the GPU completes the work.
- Complete the previous work for an upload slot before you use it again in
Painter.prepare. CallPainter.destroyAfterWaitonly after all GPU work is complete. Frame.deinitaborts a frame that did not end. It is safe on copied handles.- Apply cursor and clipboard effects one time. The host decides when to redraw or close a window.
- Custom backends must validate packet extensions and reject unsupported commands.
The bundled renderer synchronizes glyph atlas uploads and releases replaced resources by upload slot. Its cache and packet data are bounded. You do not need to acknowledge glyph uploads.
The browser build uses the same App and frame callback. Install the web host
with Knots.installWeb:
const Knots = @import("knots");
const knots = b.dependency("knots", .{
.target = target,
.optimize = optimize,
.web_threads = true,
});
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "knots", .module = knots.module("knots") },
.{ .name = "ui", .module = knots.module("ui") },
},
});
const exe = b.addExecutable(.{ .name = "app", .root_module = exe_mod });
exe.entry = .disabled;
Knots.installWeb(b, knots, exe_mod, exe, .{});Build with:
zig build -Dtarget=wasm32-freestandingA browser build exports a start function instead of main. Read
Compile & distribute for the
entry point and the HTML page.
Threaded builds need a cross-origin isolated page:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Set .web_threads = false for a build without shared memory.
- examples/playground: a component catalog and HMR host.
- examples/embedded: a host that owns its render passes.
- examples/triangle: drawing with the
Canvascomponent. - examples/benchmark: a stress test with Tracy zones.