exastencil/zigdom
Simple library for constructing DOM elements in Zig and rendering to strings
A Zig library for constructing DOM trees as pure values (no internal allocations) and rendering them to a writer or to a string.
Build type-safe, reusable HTML components using functions and enums — no template files.
Add ZigDOM to your build.zig.zon dependencies (using Zig's package manager):
.dependencies = .{
.zigdom = .{
// Use `zig fetch --save https://github.com/exastencil/zigdom/archive/refs/heads/main.tar.gz`
// then copy the generated .hash here
.url = "https://github.com/exastencil/zigdom/archive/refs/heads/main.tar.gz",
.hash = "...",
},
};
Then in your build.zig:
const zigdom_dep = b.dependency("zigdom", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zigdom", zigdom_dep.module("zigdom"));
Minimum Zig version: see build.zig.zon (currently 0.15.0).
const std = @import("std");
const zigdom = @import("zigdom");
const dom = zigdom.dom;
const tags = zigdom.tags;
const attr = dom.attr;
const text = dom.text;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Build a simple HTML structure as pure values
const page = tags.div(&.{ attr("class", "container") }, &.{
tags.p(&.{}, &.{ text("Hello, World!") }),
});
// Render to stdout (no allocation in ZigDOM)
const stdout = std.io.getStdOut().writer();
try page.render(stdout);
try stdout.writeAll("\n");
// Or render to a string (allocates; you free it)
const html = try page.renderToString(allocator);
defer allocator.free(html);
}
ZigDOM supports the following node types via dom.Tag:
Note: There is currently no separate Comment node.
Use helpers from dom and tags:
const dom = zigdom.dom;
const tags = zigdom.tags;
const attr = dom.attr;
const text = dom.text;
// Element via tag enum
const el = dom.tag(.div, &.{ attr("class", "box") }, &.{});
// Nicer syntax via generated tag functions
const div = tags.div(&.{ attr("class", "box") }, &.{});
// Text node
const t = text("Hello!");
// Fragment
const frag = dom.tag(.fragment, &.{}, &.{ div, t });
// Custom element
const custom = dom.custom("my-widget", &.{ attr("data-id", "123") }, &.{ text("Content") });
Nodes are plain values you construct with attributes and children slices:
const card = tags.div(
&.{ attr("class", "card") },
&.{
tags.h2(&.{}, &.{ text("Title") }),
tags.p(&.{}, &.{ text("Body text") }),
},
);
Void elements (like img, br, hr, ...) are handled automatically; they render without a closing tag. There is no self_closing flag to set.
// Render to any writer (no allocation inside ZigDOM)
try node.render(writer);
// Render to a string (allocates; you must free)
const html = try node.renderToString(allocator);
defer allocator.free(html);
dom.Attribute) and children are provided as slices you own.render/renderToString finishes.render(writer) does not allocate within ZigDOM.renderToString(allocator) allocates a buffer and returns an owned slice that you must free with the same allocator.deinit for Node; simply let values go out of scope. Only free what you allocated (e.g. strings you created and the result of renderToString).You can create reusable component functions that return dom.Node values:
const std = @import("std");
const zigdom = @import("zigdom");
const dom = zigdom.dom;
const tags = zigdom.tags;
const attr = dom.attr;
const text = dom.text;
const Node = dom.Node;
fn Card(title: []const u8, content: []const u8) Node {
return tags.div(&.{ attr("class", "card") }, &.{
tags.h2(&.{}, &.{ text(title) }),
tags.p(&.{}, &.{ text(content) }),
});
}
zig build test
zig build run
Contributions are welcome! Please feel free to submit a Pull Request.