Overview
This reference documents the ZX templating syntax and APIs. For a hands-on introduction, see the Learn guide.
Expressions
Use curly braces {expression} to embed any Zig expression. Values are automatically formatted based on their type and HTML-escaped for safety.
pub fn Expressions(allocator: zx.Allocator) !zx.Component {
const string_val = "hello";
const int_val: i32 = 42;
const float_val: f32 = 3.14;
const bool_true = true;
const bool_false = false;
const optional_val: ?[]const u8 = "present";
const optional_null: ?[]const u8 = null;
const InputType = enum { text, number, checkbox };
const enum_val = InputType.text;
const component_val = (<p>hi</p>);
const component_array_val = try allocator.alloc(zx.Component, 2);
component_array_val[0] = (<li>hi</li>);
component_array_val[1] = (<li>hello</li>);
return (
<form @allocator={allocator}>
<p>String: {string_val}</p>
<p>Integer: {int_val}</p>
<p>Float: {float_val}</p>
<p>Boolean: {bool_true}</p>
<p>Boolean: {bool_false}</p>
<p>Optional: {optional_val}</p>
<p>Optional: {optional_null}</p>
<p>Enum: {enum_val}</p>
<p>Component: {component_val}</p>
<ol>Component Array: {component_array_val}</ol>
</form>
);
}
const zx = @import("zx");
<form><p>String: hello</p><p>Integer: 42</p><p>Float: 3.14</p><p>Boolean: true</p><p>Boolean: false</p><p>Optional: present</p><p>Optional: </p><p>Enum: text</p><p>Component: <p>hi</p></p><ol>Component Array: <li>hi</li><li>hello</li></ol></form>Strings
String values ([]const u8) are displayed as text and automatically HTML-escaped to prevent XSS attacks. This is the safe default for displaying user input or any dynamic text content.
const name = "Alice";
...
(<p>{name}</p>) // <p>Alice</p>Integers
Integer types (i32, u64, comptime_int, etc.) are formatted as decimal numbers. The special case u8 is printed as a character if it's a printable ASCII character.
const count: u32 = 42;
...
(<p>{count}</p>) // <p>42</p>Floats
Floating-point types (f32, f64, comptime_float) are formatted as decimal numbers.
const price: f32 = 19.99;
...
(<p>${price}</p>) // <p>$19.99</p>Booleans
Boolean values are displayed as the literal strings "true" or "false".
const active = true;
...
(<p>{active}</p>) // <p>true</p>Enums
Enum values are displayed as their tag name. This is useful for displaying status values, categories, or any enumerated type.
const Status = enum { pending, approved, rejected };
const status = Status.approved;
...
(<p>{status}</p>) // <p>approved</p>Optionals
Optional values (?T) are automatically unwrapped and rendered if present. If the value is null, nothing is rendered. This makes it easy to conditionally display content.
const maybe_name: ?[]const u8 = "Bob";
...
(<p>{maybe_name}</p>) // <p>Bob</p>
const no_name: ?[]const u8 = null;
...
(<p>{no_name}</p>) // <p></p> (empty)Null
The null value renders nothing, producing no output in the final HTML.
Components
A zx.Component value is rendered directly. This allows you to embed components returned from functions or stored in variables.
const header = Header(allocator);
...
(<div>{header}</div>) // Renders the Header componentComponent Arrays
Arrays or slices of components ([]zx.Component) are rendered as a fragment containing all the components in sequence.
const items: []zx.Component = &.{ Item("A"), Item("B") };
...
(<ul>{items}</ul>) // Renders both Item componentsControl Flow
ZX supports conditional rendering and iteration using familiar Zig control flow constructs. These expressions allow you to conditionally render components or iterate over collections to build dynamic UIs.
If Statements
Use if expressions to conditionally render components based on boolean conditions. Theelse branch is optional and can render alternative content when the condition is false.
pub fn Conditional(allocator: zx.Allocator) zx.Component {
const is_admin = true;
const is_logged_in = false;
return (
<main @allocator={allocator}>
<section>
{if (is_admin) (<p>Admin</p>) else (<p>User</p>)}
</section>
<section>
{if (is_admin) ("Powerful") else ("Powerless")}
</section>
<section>
{if (is_logged_in) (
<p>Welcome, User!</p>
) else (
<p>Please log in to continue.</p>
)}
</section>
</main>
);
}
const zx = @import("zx");
<main><section><p>Admin</p></section><section>Powerful</section><section><p>Please log in to continue.</p></section></main>Switch Statements
Use switch expressions to match against enum values or other types. Each case can return either a string literal or a component. Switch expressions are particularly useful for rendering different UI based on state or user roles.
pub fn RoleSwitch(allocator: zx.Allocator) zx.Component {
const user_swtc = users[0];
return (
<main @allocator={allocator}>
<section>
{switch (user_swtc.user_type) {
.admin => ("Admin"),
.member => ("Member"),
}}
</section>
<section>
{switch (user_swtc.user_type) {
.admin => (<p>Powerful</p>),
.member => (<p>Powerless</p>),
}}
</section>
</main>
);
}
const zx = @import("zx");
const UserType = enum { admin, member };
const User = struct { name: []const u8, age: u32, user_type: UserType };
const users = [_]User{
.{ .name = "John", .age = 20, .user_type = .admin },
.{ .name = "Jane", .age = 21, .user_type = .member },
};
<main><section>Admin</section><section><p>Powerful</p></section></main>For Loops
Use for loops to iterate over arrays, slices, or strings and render a component for each item. The loop variable can be used within the component body to display item-specific content. This is ideal for rendering lists, tables, or any repeating UI patterns.
pub fn UserList(arena: zx.Allocator) zx.Component {
const users = [_]struct { name: []const u8, role: UserRole }{
.{ .name = "John", .role = .admin },
.{ .name = "Jane", .role = .member },
.{ .name = "Jim", .role = .guest },
};
return (
<main @allocator={arena}>
{for (users) |user| (
<div>
<p>{user.name}</p>
{switch (user.role) {
.admin => (<span>Admin</span>),
.member => (<span>Member</span>),
.guest => (<span>Guest</span>),
}}
</div>
)}
</main>
);
}
const zx = @import("zx");
const UserRole = enum { admin, member, guest };
<main><div><p>John</p><span>Admin</span></div><div><p>Jane</p><span>Member</span></div><div><p>Jim</p><span>Guest</span></div></main>While Loops
Use while loops for conditional iteration with a continuation expression. The syntax follows Zig's while loop pattern: while (condition) : (continue_expr) (body). This is useful for generating numbered sequences or iterating until a condition is met.
pub fn Counter(allocator: zx.Allocator) zx.Component {
var i: usize = 0;
return (
<ul @allocator={allocator}>
{while (i < 3) : (i += 1) (
<li>Item {i}</li>
)}
</ul>
);
}
const zx = @import("zx");
<ul><li>Item 0</li><li>Item 1</li><li>Item 2</li></ul>Optional Capture
Use payload capture syntax to unwrap optional values in if and while expressions:
const user_name: ?[]const u8 = "Alice";
...
{if (user_name) |name| (<p>Welcome, {name}!</p>)}
// With else branch:
{if (user_name) |name| (<p>Hello, {name}</p>) else (<p>Guest</p>)}
// While with capture (iterator pattern):
{while (iter.next()) |item| (<li>{item}</li>)}If the optional is null, the body is skipped (or the else branch runs if provided).
Error Capture
Capture errors from error unions using else |err| syntax:
// If with error capture:
{if (fetchUser()) |user| (
<p>Welcome, {user.name}!</p>
) else |err| (
<p>Error: {@errorName(err)}</p>
)}
// While with error capture:
{while (iter.next()) |item| (
<li>{item}</li>
) else |err| (
<p>Iteration ended: {@errorName(err)}</p>
)}This works with both if and while expressions for handling fallible operations.
Components
Components are reusable functions that return zx.Component. They allow you to encapsulate UI logic and create modular, maintainable code. Components can accept props (properties) to customize their behavior and appearance.
Component Function Signatures
Component functions can use one of several signatures depending on their needs:
- Allocator only:
fn Component(allocator: zx.Allocator) zx.Component - With props:
fn Component(allocator: zx.Allocator, props: Props) zx.Component - Context (children only):
fn Component(ctx: *zx.ComponentContext) zx.Component - Context with props:
fn Component(ctx: *zx.ComponentCtx(Props)) zx.Component
ComponentContext
Use zx.ComponentContext when your component needs access to children but has no custom props:
// ComponentContext: children only, no custom props
fn Wrapper(ctx: *zx.ComponentContext) zx.Component {
return (
<div @allocator={ctx.allocator} class="wrapper">
{ctx.children}
</div>
);
}ComponentCtx with Props
Use zx.ComponentCtx(Props) when you need both custom props and children access:
// ComponentCtx(Props): props and children access
const CardProps = struct { title: []const u8 };
fn Card(ctx: *zx.ComponentCtx(CardProps)) zx.Component {
return (
<article @allocator={ctx.allocator} class="card">
<h2>{ctx.props.title}</h2>
{ctx.children}
</article>
);
}The context provides ctx.allocator, ctx.props, and ctx.children.
Defining Components
To create a component, define a function that returns a zx.Component. The function must have an allocator as its first parameter. If your component needs configuration, add a props struct as the second parameter:
pub fn ButtonDemo(ctx: zx.PageContext) zx.Component {
const allocator = ctx.arena;
return (
<main @allocator={allocator}>
<Button title="Submit" class="primary-btn" />
<Button title="Cancel" />
<Button />
</main>
);
}
const ButtonProps = struct {
title: []const u8 = "Click Me",
class: []const u8 = "btn",
};
fn Button(allocator: zx.Allocator, props: ButtonProps) zx.Component {
return (
<button @allocator={allocator} class={props.class}>
{props.title}
</button>
);
}
const zx = @import("zx");
<main><button class="primary-btn">Submit</button><button class="btn">Cancel</button><button class="btn">Click Me</button></main>Props Coercion
ZX automatically coerces the attributes you pass to a component into the expected props struct. Missing required fields will result in a compile-time error, while fields with default values are optional. This provides type safety while keeping component usage flexible.
Routing
ZX provides a file-based routing system that maps URL paths to page components. Routes can be nested, and layouts can be applied hierarchically to wrap pages with common UI elements.
Pages
Pages are the main content components for each route. A page function must:
- Accept a
zx.PageContextas its only parameter - Return a
zx.Component - Be exported as
pub fn Page
// This example shows a page component
pub fn Page(ctx: zx.PageContext) zx.Component {
const allocator = ctx.arena;
// Access request data (e.g., query params, headers, etc.)
// const query = ctx.request.query() catch unreachable;
return (
<main @allocator={allocator}>
<h1>Hello, World!</h1>
<p>Welcome to the page</p>
</main>
);
}
const zx = @import("zx");
<main><h1>Hello, World!</h1><p>Welcome to the page</p></main>Layouts
Layouts wrap pages with common UI elements like headers, footers, or navigation. A layout function must:
- Accept a
zx.LayoutContextas the first parameter - Accept a
zx.Component(the page content) as the second parameter - Return a
zx.Component - Be exported as
pub fn Layout
pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
return (
<html @allocator={ctx.arena}>
<head>
<title>My App</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>
{children}
</main>
<footer>© 2025 My App</footer>
</body>
</html>
);
}
const zx = @import("zx");
// This example shows a page component
pub fn Page(ctx: zx.PageContext) zx.Component {
const allocator = ctx.arena;
// Access request data (e.g., query params, headers, etc.)
// const query = ctx.request.query() catch unreachable;
return (
<main @allocator={allocator}>
<h1>Hello, World!</h1>
<p>Welcome to the page</p>
</main>
);
}
const zx = @import("zx");
<html><head><title>My App</title></head><body><nav><a href="/">Home</a><a href="/about">About</a></nav><main>Hello, World!</main><footer>© 2025 My App</footer></body></html>PageContext and LayoutContext
Both PageContext and LayoutContext provide access to:
request: The HTTP request object with headers, query params, body, etc.response: The HTTP response object for setting headers and writing the responseallocator: Global allocator for persistent allocations (freed manually)arena: Request-scoped allocator that's automatically freed after the request (recommended for most use cases)
Use ctx.arena for temporary allocations that only need to persist during request processing. This is the recommended allocator for most page and component code.
Builtin Attributes
ZX provides special builtin attributes that control component behavior and memory management. These attributes are prefixed with @ to distinguish them from regular HTML attributes.
@allocator
The @allocator attribute is required to be passed to the topmost component. All child components will inherit the allocator from the parent component. It is available to all child components and expressions within that component tree.
Why is @allocator needed?
ZX components can allocate memory for various purposes:
- Storing text content (which is HTML-escaped and allocated)
- Copying child components arrays
- Copying attribute arrays
- Formatting expressions that allocate formatted strings
Usage:
pub fn AllocatorDemo(ctx: zx.PageContext) zx.Component {
return (
<html @allocator={ctx.arena}>
<head>
<title>My Page</title>
</head>
<body>
<MyComponent />
</body>
</html>
);
}
fn MyComponent(allocator: zx.Allocator) zx.Component {
const someText = "This text will be allocated and escaped";
return (
<div @allocator={allocator}>
{someText}
</div>
);
}
const zx = @import("zx");
<html><head><title>My Page</title></head><body><div>This text will be allocated and escaped</div></body></html>Best Practices:
- Always use
ctx.arenain page components (it's automatically freed after the request) - Pass the allocator parameter to custom components and use it in the
@allocatorattribute - Set
@allocatoron the root element of each component that needs memory allocation - Child components inherit the allocator from their parent's component, so you don't need to pass it explicitly to every nested component
@escaping
The @escaping attribute controls how text content is escaped within an element. By default, all text is HTML-escaped to prevent XSS attacks.
Values:
.html- Standard HTML escaping (default behavior).none- No escaping; outputs raw HTML. Use with caution for trusted content only.
pub fn RawHtml(allocator: zx.Allocator) zx.Component {
return (
<main @allocator={allocator}>
<div @escaping={.none}>
<strong>This HTML is rendered directly</strong>
</div>
</main>
);
}
const zx = @import("zx");
<main><div><strong>This HTML is rendered directly</strong></div></main>Warning: Only use @escaping={.none} for content you trust completely. User-provided content should always use the default escaping.
@rendering
The @rendering attribute enables client-side rendering for interactive components.
Values:
.react- Client-side render with React (for imported React/TSX components).client- Client-side render with Zig (compile component to WebAssembly)
pub fn ReactDemo(allocator: zx.Allocator) zx.Component {
const max_count = 10;
return (
<main @allocator={allocator}>
<CounterComponent @rendering={.react} max_count={max_count} />
</main>
);
}
const zx = @import("zx");
const CounterComponent = @jsImport("react.tsx");
<main><!--$c40f580 CounterComponent {"max_count":10}--><!--/$c40f580--></main>Attribute Syntax
ZX provides convenient syntax for working with attributes beyond simple static values.
Spread Attributes
Use {..struct} to spread all fields of a struct as attributes on an element. This is useful for passing groups of attributes or forwarding props:
const form_attrs = .{
.class = "form-control",
.@"data-validate" = "true",
};
const input_props = .{ .name = "email", .value = "test@example.com" };
...
(<form {..form_attrs}>
<input type="text" {..input_props} />
</form>)Spread attributes can be combined with explicit attributes. Explicit attributes take precedence when there are conflicts.
Shorthand Attributes
When the attribute name matches the variable name, use the shorthand syntax {variable}:
const class = "btn-primary";
const disabled = true;
const @"data-id" = "123";
...
(<button {class} {disabled} {@"data-id"}>Click</button>)
// Renders: <button class="btn-primary" disabled data-id="123">Click</button>This is equivalent to writing class={class} but more concise.
Template Attributes
Use backticks (`) for string interpolation in attribute values. Embed expressions with {expr} inside the template:
const count = 42;
const name = "John";
...
(<div class=`item-{count}` data-user=`user-{name}`>
<a href=`/users/{name}/profile`>Profile</a>
</div>)
// Renders: <div class="item-42" data-user="user-John">
// <a href="/users/John/profile">Profile</a>
// </div>Template attributes are useful for building dynamic URLs, class names, or data attributes that combine static text with dynamic values.
Importing
ZX supports importing components and modules from various sources. You can import ZX components from other files, React/TSX components for client-side interactivity, and standard Zig modules.
pub fn ImportDemo(allocator: zx.Allocator) zx.Component {
return (
<main @allocator={allocator}>
<Button title="Click me" />
<Card title="Welcome">
<p>Card content here</p>
</Card>
</main>
);
}
const zx = @import("zx");
// Define components in the same file
const ButtonProps = struct { title: []const u8 };
fn Button(a: zx.Allocator, props: ButtonProps) zx.Component {
return (<button @allocator={a} class="btn">{props.title}</button>);
}
const CardProps = struct { title: []const u8, children: zx.Component };
fn Card(a: zx.Allocator, props: CardProps) zx.Component {
return (
<div @allocator={a} class="card">
<h2>{props.title}</h2>
{props.children}
</div>
);
}
<main><button class="btn">Click me</button><div class="card"><h2>Welcome</h2><p>Card content here</p></div></main>Use @jsImport for React/TSX components and the standard @import for ZX and Zig files.
Dynamic Routes
ZX supports dynamic route segments using bracket notation in folder names.
Creating Dynamic Routes
Create a folder with brackets around the parameter name:
site/pages/user/[id]/page.zx→/user/:idsite/pages/blog/[slug]/page.zx→/blog/:slugsite/pages/[category]/[item]/page.zx→/:category/:item
Accessing Route and Query Parameters
Use ctx.request.getParam("name") to access dynamic route parameters and ctx.request.query("name") for query strings:
pub fn UserProfile(ctx: zx.PageContext) zx.Component {
const user_id = ctx.request.getParam("id") orelse "unknown";
return (
<main @allocator={ctx.arena}>
<h1>User Profile</h1>
<p>User ID: {user_id}</p>
</main>
);
}
const zx = @import("zx");
<main><h1>User Profile</h1><p>User ID: unknown</p></main>Both methods return ?[]const u8, so use orelse to provide a default value.
Fragments
Fragments allow you to return multiple elements from a component without adding an extra wrapper element to the DOM. Use the empty tag syntax <>...</> to create a fragment:
pub fn FragmentDemo(allocator: zx.Allocator) zx.Component {
return (
<main @allocator={allocator}>
<Header />
</main>
);
}
fn Header(allocator: zx.Allocator) zx.Component {
return (
<>
<h1 @allocator={allocator}>Welcome</h1>
<p>Multiple elements without a wrapper</p>
</>
);
}
const zx = @import("zx");
<main><h1>Welcome</h1><p>Multiple elements without a wrapper</p></main>The fragment's children are rendered directly without any containing element.
Children Props
Components can accept child elements through a children prop, enabling wrapper and container patterns. Any elements between the opening and closing tags are passed as the children prop:
pub fn CardDemo(allocator: zx.Allocator) zx.Component {
return (
<main @allocator={allocator}>
<Card title="Welcome">
<p>This is the card content.</p>
<button>Click me</button>
</Card>
</main>
);
}
const CardProps = struct {
title: []const u8,
children: zx.Component,
};
fn Card(allocator: zx.Allocator, props: CardProps) zx.Component {
return (
<div @allocator={allocator} class="card">
<h2>{props.title}</h2>
<div class="card-body">{props.children}</div>
</div>
);
}
const zx = @import("zx");
<main><div class="card"><h2>Welcome</h2><div class="card-body"><p>This is the card content.</p><button>Click me</button></div></div></main>Syntax Features
ZX supports additional Zig syntax features within templates.
Multiline Strings
Use Zig's multiline string syntax (\\) to embed formatted text blocks:
// Multiline strings use Zig's \\ syntax:
(<pre>{
\\const x = 1;
\\const y = 2;
\\const sum = x + y;
}</pre>)Each line prefixed with \\ is concatenated. This is useful for code snippets, preformatted text, or ASCII art.
Comments
Single-line comments are supported within ZX templates:
// Single-line comments are supported in ZX:
(<div>
// This is a comment
<p>Visible content</p>
// Another comment
</div>)Comments are stripped from the output and do not appear in the rendered HTML.
Caching Alpha
ZX provides built-in caching for components, pages, and layouts to improve performance. Cached content is stored in memory and served directly without re-rendering.
Overview
Caching can be applied at three levels:
- Component caching — Cache individual components using the
@cachingattribute - Page caching — Cache entire pages using
PageOptions.caching - Layout caching — Cache layouts using
LayoutOptions.caching
Component Caching
Use the @caching attribute on any component to cache its rendered HTML output. The syntax is <duration> or <duration>:key.
Time Units
| Unit | Description | Example |
|---|---|---|
s | Seconds | "10s" = 10 seconds |
m | Minutes | "5m" = 300 seconds |
h | Hours | "2h" = 7200 seconds |
d | Days | "1d" = 86400 seconds |
Basic Usage
// Cache for 10 seconds
(<Header @caching="10s" />)
// Cache for 5 minutes
(<Sidebar @caching="5m" />)
// Cache for 1 hour
(<Footer @caching="1h" />)
// Cache for 1 day
(<StaticContent @caching="1d" />)Custom Cache Keys
Add a custom key after the duration with a colon (:) separator. Keys are useful for manual cache invalidation:
// Cache with a custom key for manual invalidation
(<UserProfile @caching="10m:user-profile" user={user} />)
// Cache navigation with key
(<Navigation @caching="1h:main-nav" />)The cache key is prefixed with cmp: internally, so "10m:user-profile" creates a key cmp:user-profile.
Page Caching
Enable page-level caching by exporting a options constant with caching settings:
pub fn Page(ctx: zx.PageContext) zx.Component {
return (
<div @allocator={ctx.arena}>
<h1>Cached Page</h1>
<p>This page is cached for 5 minutes.</p>
</div>
);
}
// Enable page-level caching
pub const options = zx.PageOptions{
.caching = .{ .seconds = 300 }, // 5 minutes
};Page caching stores the entire rendered response including headers. The server adds ETag and Cache-Control headers automatically.
Layout Caching
Layouts can also be cached using LayoutOptions:
pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
return (
<html @allocator={ctx.arena}>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// Cache the entire layout for 1 hour
pub const options = zx.LayoutOptions{
.caching = .{ .seconds = 3600 },
};Cache Invalidation
Manually invalidate cache entries using zx.cache functions:
// Delete a specific cache entry by key
_ = zx.cache.del("cmp:user-profile");
// Delete all entries matching a prefix
// Useful for invalidating related content
_ = zx.cache.delPrefix("cmp:user");Available functions:
zx.cache.del(key)— Delete a specific cache entry by exact key. Returnstrueif deleted.zx.cache.delPrefix(prefix)— Delete all entries matching a prefix. Returns the count of deleted entries.zx.cache.get(key)— Get cached HTML by key. Returns?[]const u8.
Best Practices
- Cache static content aggressively — Headers, footers, navigation, and other rarely-changing content benefit most from caching.
- Use custom keys for related content — Group related cache entries with a common prefix for easy bulk invalidation.
- Be careful with user-specific content — Don't cache components that display user-specific data without including user identity in the cache key.
- Start with short TTLs — Begin with shorter cache durations and increase as you verify correctness.
- Invalidate on data changes — Call
zx.cache.del()orzx.cache.delPrefix()when underlying data changes.
Plugins
ZX provides a plugin system to extend the build process with additional tools. Plugins run as build steps and integrate seamlessly with zig build.
Overview
Plugins are configured in your build.zig file and passed to zx.init(). Each plugin adds build steps that run before or after ZX transpilation, allowing you to process CSS, bundle JavaScript, or run other tools.
ZX provides builtin plugins for common tasks, and you can also create custom plugins for your specific needs.
Builtin Plugins
ZX ships with the following builtin plugins available via zx.plugins:
zx.plugins.tailwind— Compile Tailwind CSS styleszx.plugins.esbuild— Bundle TypeScript/JavaScript for client-side code
Tailwind CSS Plugin
The tailwind builtin plugin compiles your CSS using the Tailwind CSS CLI. It automatically runs after ZX transpiles your pages.
Options
| Option | Type | Default | Description |
|---|---|---|---|
bin | ?LazyPath | node_modules/.bin/tailwindcss | Path to the Tailwind CLI binary |
input | ?LazyPath | site/assets/styles.css | Input CSS file with Tailwind directives |
output | ?LazyPath | {outdir}/assets/styles.css | Output path for compiled CSS |
minify | bool | false | Optimize and minify the output |
optimize | bool | false | Optimize output without minifying |
cwd | ?LazyPath | null | Working directory for Tailwind |
map | bool | false | Generate a source map |
Example
const zx = @import("zx");
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const zx_dep = b.dependency("zx", .{ .target = target, .optimize = optimize });
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("site/main.zig"),
.target = target,
.optimize = optimize,
}),
});
try zx.init(b, exe, .{
.plugins = &.{
zx.plugins.tailwind(b, .{
.bin = b.path("node_modules/.bin/tailwindcss"),
.input = b.path("site/assets/styles.css"),
.output = b.path("{outdir}/assets/styles.css"),
.minify = optimize != .Debug,
}),
},
});
}esbuild Plugin
The esbuild builtin plugin bundles TypeScript or JavaScript files for client-side code. It supports modern JavaScript features and can bundle dependencies.
Options
| Option | Type | Default | Description |
|---|---|---|---|
bin | ?LazyPath | node_modules/.bin/esbuild | Path to the esbuild binary |
input | ?LazyPath | site/main.ts | Entry point file |
output | ?LazyPath | {outdir}/assets/main.js | Output file path |
bundle | bool | true | Bundle all dependencies |
minify | ?bool | true in release | Minify the output |
sourcemap | ?enum | inline in debug | Source map mode: none, inline, external, linked, both |
log_level | ?enum | silent | Log level: verbose, debug, info, warning, error, silent |
format | ?enum | inferred | Output format: iife, cjs, esm |
platform | ?enum | browser | Target platform: browser, node, neutral |
target | ?[]const u8 | esnext | Environment target (e.g., es2017, chrome58) |
splitting | bool | false | Enable code splitting (ESM only) |
external | []const []const u8 | &.{} | Modules to exclude from bundle |
define | []const struct | &.{} | Custom substitutions (key-value pairs) |
Automatic defines: The esbuild plugin automatically sets __DEV__ and process.env.NODE_ENV based on the build mode.
Example
const zx = @import("zx");
const std = @import("std");
pub fn build(b: *std.Build) !void {
// ... setup code ...
try zx.init(b, exe, .{
.plugins = &.{
zx.plugins.esbuild(b, .{
.bin = b.path("node_modules/.bin/esbuild"),
.input = b.path("site/main.ts"),
.output = b.path("{outdir}/assets/main.js"),
.bundle = true,
.format = .esm,
.platform = .browser,
.target = "es2020",
.external = &.{ "react", "react-dom" },
.define = &.{
.{ .key = "API_URL", .value = "\"https://api.example.com\"" },
},
}),
},
});
}
Combining Plugins
You can use multiple plugins together. They run in order after ZX transpilation:
.plugins = &.{
zx.plugins.esbuild(b, .{
.input = b.path("site/main.ts"),
.output = b.path("{outdir}/assets/main.js"),
}),
zx.plugins.tailwind(b, .{
.input = b.path("site/assets/styles.css"),
.output = b.path("{outdir}/assets/styles.css"),
}),
},
});
Creating Custom Plugins
You can create your own plugins by returning a zx.ZxInitOptions.PluginOptions struct. This allows you to integrate any command-line tool into the ZX build process.
Plugin Structure
A plugin consists of a name and a list of steps. Each step specifies when to run (before or after transpilation) and what command to execute:
const ExperimentalOptions = struct {
/// Enable Client-Side Rendering (CSR) support.
///
/// When enabled, ZX will compile a WebAssembly module for client-side
/// interactivity and hydration. This generates additional build artifacts
/// in the assets directory.
///
/// Default: `false`
enabled_csr: bool = false,
};
/// Configuration for build plugins that extend ZX functionality.
pub const PluginOptions = struct {
/// Command-based plugin step configuration.
pub const PluginStepCommand = struct {
/// When to execute this plugin in the build lifecycle.
type: enum {
/// Run before ZX transpilation occurs
before_transpile,
/// Run after ZX transpilation completes
after_transpile,
},
/// Command to execute.
///
/// Use `{outdir}` in `LazyPath` arguments to reference the transpile output directory.
run: *std.Build.Step.Run,
};
Example: Custom Image Optimizer Plugin
Here's how to create a custom plugin that optimizes images during the build:
const std = @import("std");
pub fn build(b: *std.Build) !void {
// ... setup code ...
try zx.init(b, exe, .{
.plugins = &.{
// Custom plugin using PluginOptions directly
createImageOptimizer(b),
},
});
}
fn createImageOptimizer(b: *std.Build) zx.ZxInitOptions.PluginOptions {
const cmd = std.Build.Step.Run.create(b, "optimize-images");
// Add your custom command
cmd.addArgs(&.{ "npx", "imagemin", "site/public/**/*", "--out-dir={outdir}/public" });
// Allocate steps array
const steps = b.allocator.alloc(zx.ZxInitOptions.PluginOptions.PluginStep, 1) catch @panic("OOM");
steps[0] = .{
.command = .{
.type = .after_transpile, // Run after ZX transpilation
.run = cmd,
},
};
return .{
.name = "image-optimizer",
.steps = steps,
};
}
Plugin Lifecycle
before_transpile— Runs before ZX transpiles.zxfiles to Zig. Useful for preprocessing source files.after_transpile— Runs after ZX transpilation. Useful for post-processing like CSS compilation, JS bundling, or asset optimization.
Using {outdir}
In output paths, use {outdir} as a placeholder for the ZX output directory.
tailwind.zig and esbuild.zig in the ZX source) for examples of how to structure complex plugins with many options.Command Line Interface
ZX comes with a built-in CLI tool to help you create, develop, and deploy your projects.
Installation
Install the ZX CLI using one of the following methods:
init
Initialize a new ZX project. Creates all the necessary files and directory structure to get started with ZX.
Arguments
[path]— Path to initialize the project in (default: current directory)
Flags
--template, -t <name>— Template to use (default: "default"). Options: default, react, wasm, react_wasm--force, -f— Force initialization even if the directory is not empty--existing— Initialize ZX in an existing project (only adds files that don't exist)
dev
Start the app in development mode with hot reloading. Watches for file changes and automatically rebuilds.
Flags
--binpath, -b <path>— Binpath of the app (default: auto-detected)--build-args <args>— Additional arguments to pass to zig build
serve
Serve the project for production. Builds and runs the executable.
Flags
--port, -p <number>— Port to run the server on (default: 3000)
transpile
Transpile .zx files to Zig source code. Useful for debugging or inspecting generated code.
Arguments
<path>— Path to .zx file or directory (required)
Flags
--outdir, -o <directory>— Output directory (default: ".zx")
fmtAlpha
Format ZX source code files according to ZX's formatting rules.
Arguments
[path]— Path to .zx file or directory (optional, formats current directory if not specified)
Flags
--stdio— Read from stdin and write to stdout--stdout— Write formatted output to stdout instead of modifying files
export
Generate static site assets. Builds your project and exports it for static hosting.
Flags
--outdir, -o <directory>— Output directory (default: "dist")--binpath, -b <path>— Binpath of the app executable (default: auto-detected)
bundle
Bundle the site into a deployable directory with the executable and assets.
Flags
--outdir, -o <directory>— Output directory (default: "bundle")--binpath, -b <path>— Binpath of the app executable (default: auto-detected)
update
Update the ZX dependency version in your project's build.zig.zon file.
Flags
--version, -v <version>— Version to update to (default: "latest")
upgrade
Upgrade the ZX CLI tool itself. Downloads and installs a new version of the CLI binary.
Flags
--version, -v <version>— Version to upgrade to (default: "latest")
version
Show the current version of the ZX CLI tool.