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.14.1).
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.