Introduction
ZX is a Zig library for building web applications with JSX-like syntax. Write declarative UI components using familiar JSX patterns, transpiled to efficient Zig code.
ZX combines the power and performance of Zig with the expressiveness of JSX, enabling you to build fast, type-safe web applications. ZX is significantly faster than frameworks like Next.js at SSR. Currently 120X faster than Next.js at SSR.
Getting Started
Installation
Install the ZX CLI tool to get started with building ZX applications.
Linux/macOS
curl -fsSL https://ziex.dev/install | bashWindows
powershell -c "irm ziex.dev/install.ps1 | iex"Installing Zig
brew install zig # macOS
winget install -e --id zig.zig # WindowsCreating a New Project
Create a new folder for your project, navigate into it, and run zx init:
mkdir my-project
cd my-project
zx init
Make sure to run zx init inside a clean project folder. After running the command, you'll see:
โ Initializing ZX project! Template: default
+ .vscode/extensions.json
+ build.zig.zon
+ build.zig
+ README.md
+ site/assets/style.css
+ site/main.zig
+ site/pages/about/page.zx
+ site/pages/layout.zx
+ site/pages/page.zx
+ src/root.zig
+ .gitignore
Now run โ
zig build serveThen run the development server:
zig build serveYou'll see the server start:
ZX ยท 0.0.1-dev.90
- Local: http://localhost:3000Editor Setup
Enhance your development experience with the official ZX extension for VSCode and Cursor. The
extension provides syntax highlighting, code completion, and other helpful features for working with
.zx files. Install it from Open VSX.
Expressions
ZX provides different expression syntaxes for embedding dynamic content. Each syntax serves a specific purpose: rendering components, displaying text safely, or formatting values with custom format specifiers.
Text Expressions
Use {expression} to embed text content. All text is automatically HTML-escaped to prevent
XSS attacks. This is the safe way to display user input or dynamic text content.
<main @allocator={allocator}>
<section>
<p>User: {user_name}</p>
<p>Safe HTML: {html_content}</p>
<p>Unsafe HTML: {[unsafe_html:s]}</p>
</section>
</main><main><section><p>User: Alice & Bob</p><p>Safe HTML: <script>alert('XSS')</script></p><p>Unsafe HTML: <span>Test</span></p></section></main>_zx.zx(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.zx(
.section,
.{
.children = &.{
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("User: "),
_zx.txt(user_name),
},
},
),
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Safe HTML: "),
_zx.txt(html_content),
},
},
),
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Unsafe HTML: "),
_zx.fmt("{s}", .{unsafe_html}),
},
},
),
},
},
),
},
},
)Format Expressions
Use {[expression:format]} to format values with custom format specifiers. The format string follows
Zig's standard format specifier syntax (e.g., d for decimal, x for
hexadecimal). Unlike text expressions, format expressions are not HTML-escaped, making them suitable
for numeric formatting and other non-HTML content.
<section @allocator={allocator}>
<p>Count: {[count:d]}</p>
<p>Hex: 0x{[hex_value:x]}</p>
<p>Percentage: {[percentage:d]}%</p>
</section><section><p>Count: 42</p><p>Hex: 0xff</p><p>Percentage: 75%</p></section>_zx.zx(
.section,
.{
.allocator = allocator,
.children = &.{
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Count: "),
_zx.fmt("{d}", .{count}),
},
},
),
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Hex: 0x"),
_zx.fmt("{x}", .{hex_value}),
},
},
),
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Percentage: "),
_zx.fmt("{d}", .{percentage}),
_zx.txt("%"),
},
},
),
},
},
)Component Expressions
Use {(expression)} to embed a component directly. The expression must evaluate to a
zx.Component type. This is useful when you want to conditionally render or reuse
components dynamically.
<section @allocator={allocator}>
{(greeting)}
</section><section>Hello!</section>_zx.zx(
.section,
.{
.allocator = allocator,
.children = &.{
greeting,
},
},
)Control 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. The
else branch is optional and can render alternative content when the condition is false.
<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><main><section><p>Admin</p></section><section><fragment>"Powerful"</fragment></section><section><p>Please log in to continue.</p></section></main>_zx.zx(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.zx(
.section,
.{
.children = &.{
if ((is_admin)) _zx.zx(
.p,
.{
.children = &.{
_zx.txt("Admin"),
},
},
) else _zx.zx(
.p,
.{
.children = &.{
_zx.txt("User"),
},
},
),
},
},
),
_zx.zx(
.section,
.{
.children = &.{
if ((is_admin)) _zx.zx(
.fragment,
.{
.children = &.{
_zx.txt("\"Powerful\""),
},
},
) else _zx.zx(
.fragment,
.{
.children = &.{
_zx.txt("\"Powerless\""),
},
},
),
},
},
),
_zx.zx(
.section,
.{
.children = &.{
if ((is_logged_in)) _zx.zx(
.p,
.{
.children = &.{
_zx.txt("Welcome, User!"),
},
},
) else _zx.zx(
.p,
.{
.children = &.{
_zx.txt("Please log in to continue."),
},
},
),
},
},
),
},
},
)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.
<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><main><section>Admin</section><section><p>Powerful</p></section></main>_zx.zx(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.zx(
.section,
.{
.children = &.{
switch (user_swtc.user_type) {
.admin => _zx.txt("Admin"),
.member => _zx.txt("Member"),
},
},
},
),
_zx.zx(
.section,
.{
.children = &.{
switch (user_swtc.user_type) {
.admin => _zx.zx(
.p,
.{
.children = &.{
_zx.txt("Powerful"),
},
},
),
.member => _zx.zx(
.p,
.{
.children = &.{
_zx.txt("Powerless"),
},
},
),
},
},
},
),
},
},
)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.
<main @allocator={ctx}>
{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><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>_zx.zx(
.main,
.{
.allocator = ctx,
.children = blk: {
const __zx_children = _zx.getAllocator().alloc(zx.Component, users.len) catch unreachable;
for (users, 0..) |user, _zx_i| {
__zx_children[_zx_i] = _zx.zx(
.div,
.{
.children = &.{
_zx.zx(
.p,
.{
.children = &.{
_zx.txt(user.name),
},
},
),
switch (user.role) {
.admin => _zx.zx(
.span,
.{
.children = &.{
_zx.txt("Admin"),
},
},
),
.member => _zx.zx(
.span,
.{
.children = &.{
_zx.txt("Member"),
},
},
),
.guest => _zx.zx(
.span,
.{
.children = &.{
_zx.txt("Guest"),
},
},
),
},
},
},
);
}
break :blk __zx_children;
},
},
)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 must follow one of two signatures:
- Single parameter (allocator only):
fn ComponentName(allocator: zx.Allocator) zx.Component - Two parameters (allocator and props):
fn ComponentName(allocator: zx.Allocator, props: PropsType) zx.Component
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:
<main @allocator={allocator}>
<Button title="Submit" class="primary-btn" />
<Button title="Cancel" />
<Button />
</main><main><button class="primary-btn">Submit</button><button class="btn">Cancel</button><button class="btn">Click Me</button></main>_zx.zx(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.lazy(Button, .{ .title = "Submit", .class = "primary-btn" }),
_zx.lazy(Button, .{ .title = "Cancel" }),
_zx.lazy(Button, .{}),
},
},
)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
<main @allocator={allocator}>
<h1>Hello, World!</h1>
<p>Welcome to the page</p>
</main><main><h1>Hello, World!</h1><p>Welcome to the page</p></main>_zx.zx(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.zx(
.h1,
.{
.children = &.{
_zx.txt("Hello, World!"),
},
},
),
_zx.zx(
.p,
.{
.children = &.{
_zx.txt("Welcome to the page"),
},
},
),
},
},
)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
<div @allocator={ctx.arena}>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css" rel="stylesheet">
<link rel="stylesheet" href={css_version}>
<header class="top-bar">
<div class="top-bar-content">
<div class="top-bar-left">
<a href="/" class="top-bar-logo">ZX</a>
</div>
<div class="top-bar-right">
<nav class="top-bar-nav">
<a href="/docs" class="top-bar-link">Docs</a>
<a href="/examples" class="top-bar-link">Examples</a>
<a href="/cli" class="top-bar-link">CLI</a>
</nav>
<div class="top-bar-actions">
<a href="https://github.com/nurulhudaapon/zx" target="_blank" rel="noopener noreferrer" class="top-bar-icon" aria-label="GitHub" title="v{zx.info.version_string}">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
</div>
</div>
</div>
</header>
<div class="container examples-container">
<main class="content examples-content">
{(children)}
</main>
</div>
<script async type="module" src={js_version}></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"/>
</div><div><link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css" rel="stylesheet"><link rel="stylesheet" href="/assets/docs.css?v=0.0.1-dev.242"><header class="top-bar"><div class="top-bar-content"><div class="top-bar-left"><a href="/" class="top-bar-logo">ZX</a></div><div class="top-bar-right"><nav class="top-bar-nav"><a href="/docs" class="top-bar-link">Docs</a><a href="/examples" class="top-bar-link">Examples</a><a href="/cli" class="top-bar-link">CLI</a></nav><div class="top-bar-actions"><a href="https://github.com/nurulhudaapon/zx" target="_blank" rel="noopener noreferrer" class="top-bar-icon" aria-label="GitHub" title="v{zx.info.version_string}"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg></svg></a></div></div></div></header><div class="container examples-container"><main class="content examples-content"><main><h1>Hello, World!</h1><p>Welcome to the page</p></main></main></div><script type="module" src="/assets/docs.js?v=0.0.1-dev.242"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"></script></div>_zx.zx(
.div,
.{
.allocator = ctx.arena,
.children = &.{
_zx.zx(
.link,
.{
.attributes = &.{
.{ .name = "href", .value = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css" },
.{ .name = "rel", .value = "stylesheet" },
},
},
),
_zx.zx(
.link,
.{
.attributes = &.{
.{ .name = "rel", .value = "stylesheet" },
.{ .name = "href", .value = css_version },
},
},
),
_zx.zx(
.header,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar" },
},
.children = &.{
_zx.zx(
.div,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar-content" },
},
.children = &.{
_zx.zx(
.div,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar-left" },
},
.children = &.{
_zx.zx(
.a,
.{
.attributes = &.{
.{ .name = "href", .value = "/" },
.{ .name = "class", .value = "top-bar-logo" },
},
.children = &.{
_zx.txt("ZX"),
},
},
),
},
},
),
_zx.zx(
.div,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar-right" },
},
.children = &.{
_zx.zx(
.nav,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar-nav" },
},
.children = &.{
_zx.zx(
.a,
.{
.attributes = &.{
.{ .name = "href", .value = "/docs" },
.{ .name = "class", .value = "top-bar-link" },
},
.children = &.{
_zx.txt("Docs"),
},
},
),
_zx.zx(
.a,
.{
.attributes = &.{
.{ .name = "href", .value = "/examples" },
.{ .name = "class", .value = "top-bar-link" },
},
.children = &.{
_zx.txt("Examples"),
},
},
),
_zx.zx(
.a,
.{
.attributes = &.{
.{ .name = "href", .value = "/cli" },
.{ .name = "class", .value = "top-bar-link" },
},
.children = &.{
_zx.txt("CLI"),
},
},
),
},
},
),
_zx.zx(
.div,
.{
.attributes = &.{
.{ .name = "class", .value = "top-bar-actions" },
},
.children = &.{
_zx.zx(
.a,
.{
.attributes = &.{
.{ .name = "href", .value = "https://github.com/nurulhudaapon/zx" },
.{ .name = "target", .value = "_blank" },
.{ .name = "rel", .value = "noopener noreferrer" },
.{ .name = "class", .value = "top-bar-icon" },
.{ .name = "aria-label", .value = "GitHub" },
.{ .name = "title", .value = "v{zx.info.version_string}" },
},
.children = &.{
_zx.zx(
.svg,
.{
.attributes = &.{
.{ .name = "width", .value = "16" },
.{ .name = "height", .value = "16" },
.{ .name = "viewBox", .value = "0 0 16 16" },
.{ .name = "fill", .value = "currentColor" },
},
.children = &.{
_zx.fmt("{s}", .{"\n <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z\"/>\n </svg>"}),
},
},
),
},
},
),
},
},
),
},
},
),
},
},
),
},
},
),
_zx.zx(
.div,
.{
.attributes = &.{
.{ .name = "class", .value = "container examples-container" },
},
.children = &.{
_zx.zx(
.main,
.{
.attributes = &.{
.{ .name = "class", .value = "content examples-content" },
},
.children = &.{
children,
},
},
),
},
},
),
_zx.zx(
.script,
.{
.attributes = &.{
.{ .name = "type", .value = "module" },
.{ .name = "src", .value = js_version },
},
},
),
_zx.zx(
.script,
.{
.attributes = &.{
.{ .name = "src", .value = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js" },
},
},
),
_zx.zx(
.script,
.{
.attributes = &.{
.{ .name = "src", .value = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js" },
},
},
),
},
},
)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:
<html @allocator={ctx.arena}>
<head>
<title>My Page</title>
</head>
<body>
<MyComponent />
</body>
</html><html><head><title>My Page</title></head><body><div>This text will be allocated and escaped</div></body></html>_zx.zx(
.html,
.{
.allocator = ctx.arena,
.children = &.{
_zx.zx(
.head,
.{
.children = &.{
_zx.zx(
.title,
.{
.children = &.{
_zx.txt("My Page"),
},
},
),
},
},
),
_zx.zx(
.body,
.{
.children = &.{
_zx.lazy(MyComponent, .{}),
},
},
),
},
},
)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