ZX - HTML syntax within Zig code, just like JSX, but for Zig!

I was looking for a cleaner way to render HTML with Zig while keeping Zig’s control flow, similar to how JSX works. Since I’m still learning Zig, I thought it’d be a good opportunity to build a prototype and learn by doing.
ZX lets you write components like this:



ZX CLI installation:

Linux/MacOS

curl -fsSL https://ziex.dev/install | bash

Windows

powershell -c "irm ziex.dev/install.ps1 | iex"

Links

Examples:

This is still very much a prototype, and there are many things I need help with or would love to discuss with the community to decide on the best approach. That’s why I’m sharing it here!

Would appreciate any feedback, ideas, or contributions! :folded_hands:

16 Likes

Hey! Looks great.

I have some questions though?

In your example, I assume it won’t compile in zig since return ( <…> ) isn’t valid zig?

Also which example file is what you write? The first file uses zig imports, but the second is a .zx file? Do you write zx file and transpire to zig or other way around?

Does this already handle state changes automatically? Are you using a virtual ui tree and reconciler? Curious where you are, and what your approach is to solving the issues of layout, state management ect.

Thank you for looking into it!

Yes it is not valid Zig but extended to just support HMTL, this is a ZX (Zig with HTML within it just like JSX) file that will be transpiled to Zig, and from the Zig code, it reconciles the element tree to HTML.

Currently, it is just SSR, so it is already useful as a template engine and can also be used to generate SSG. For client-side state management (which it doesn’t do), I’m thinking of two solutions: either compile Zig to WASM for the client-side part and have a bridge with JS to update the DOM or just let the user write React/Svelte for the client-side part using JS and be able to import those components and use them within ZX components or vice versa.

Take a look at the doc where I’ve example of ZX > Zig > HTML. https://zx.nuhu.dev

Also you can try pulling this example and run zig build serve to see it in action: GitHub - nurulhudaapon/zigx.nuhu.dev: A demo web application built using ZigX! - Live: https://zigx.nuhu.dev

I think in general, you may want to compile to WASM, I personally, wouldn’t see a purpose in using a framework, that I can’t write zig in, but transpiles to zig, which then transpiles to html. It would seem like a lot of extra steps just to get to a point where I am writing React or Svelte. If you really want to have the performance benefits of Zig, maybe compile a wasm binary, and build a JS bridge, the overhead of going from WASM to JS, is negligible at this point. At the end of the day, everything is just html, and React, Svelte, whatever, just generate html, the questions is where the performance exists.

A big reason state management exists, is due to Javascripts lack of performance, you could try building a mini immediate mode gui, where the entire UI tree is reconciled and rendered on each pass. This is what CLAY: GitHub - nicbarker/clay: High performance UI layout library in C. has done.

Also in my humble opinion, perhaps consider what you want out of this framework. Zine Zine: A Static Site Generator Written in Zig | Loris Cro's Blog already does great SSG.

  1. Are you looking to just learn, or do you want to build a full production grade framework?

  2. Is the idea to use React for basic client side rendering, and then make use of WASM generated from Zig for performance? Or do you want WASM and Zig to handle the whole stack?

  3. Most importantly what problem are you trying to solve? For example Golang Templ: GitHub - a-h/templ: A language for writing HTML user interfaces in Go., is a template engine for ssr in golang, which seems similar to your project?

I don’t want to be discouraging, but I also don’t want Zig, to become another Javascript ecosystem, where there are 1000 of frameworks, libraries, all doing something slightly different.

Overall, I think the project is a really cool idea, but it also seems like it doesn’t really know what it wants to be?

You can also look into Zig itself as a way to define your UI, for example you can return functions, that capture there children through a void argument like so:


////////////// Here is an example where Zig itself is the UI, and then you could use Zig itself to create UIs
    Node.style(&.{
        .size = .{ .width = .percent(50), .height = .percent(100) }, // 500
    })({
        Node.style(&.{
            .size = .{ .width = .fit, .height = .percent(100) }, // 500
        })({
            Node.style(&.{
                .size = .{ .width = .px(500), .height = .percent(100) },
            })({});
        });
        Node.style(&.{
            .size = .{ .width = .grow, .height = .percent(100) }, // 0
        })({});
    });

///////////////////////////////////////////////////////////////////////////
 
const Scram = @import("Scram.zig");
const Style = @import("Style.zig").Style;
const Node = Scram.Node;

/// The LifeCycle struct
/// allows control over ui node in the tree
/// exposes open, configure, and close, must be called in this order to attach the node to the tree
pub const LifeCycle = struct {
    /// open takes an element decl and return a *UINode
    /// this opens the element to allow for children
    /// within the dom tree, node this current opened node is the current top stack node, ie any children
    /// will reference this node as their parent
    pub fn open() ?*Node {
        const ui_node = Scram.instance.open() catch {
            return null;
        };
        return ui_node;
    }
    /// close, closes the current UINode
    pub fn close(_: void) void {
        _ = Scram.instance.close();
        return;
    }
    /// configure is used internally to configure the UINode, used for adding text props, or hover props ect
    /// within configure, we check if the node has a id if so we use that, otherwise later we generate one
    /// we also set various props, such as text, style, is an SVG or not
    /// Any mainpulation of the node after this point is considered undefined behaviour be cautious;
    pub fn configure(style_ptr: ?*const Style) void {
        _ = Scram.instance.configure(style_ptr);
    }
};

const Self = @This();
pub inline fn style(_: *const Self, style_ptr: *const Style) fn (void) void {
    _ = LifeCycle.open() orelse unreachable;
    LifeCycle.configure(style_ptr);
    return LifeCycle.close;
}
1 Like

Are you misunderstanding the project and possibly what jsx is?
(I also only know it roughly), but as far as I understand it jsx is a superset of javascript that adds html expression syntax and this project aims to be a superset of Zig that adds html expression syntax (so you can write Zig in zx, but generally you would only write code that does basic templating, because usually you don’t want huge amounts of code in templates).

It doesn’t seem to be your intention but I find your comment comes across as a bit condescending.

I think that is something we can at most inspire, but ultimately people want to work on the things they want to work on, I don’t think this will be avoidable as the number of people using Zig increases, I think you will just have to pick some parts of the ecosystem which you agree with and let others choose and create what they want to use.

I think the best you can do is to try and strive to create something which doesn’t have too crazy dependencies and has good quality, but still I think you will have to sift between different projects of varying quality and multiple projects doing similar things. For example Zig already has a ton of different cli-argument parsers and I think that is okay.

Personally I find the syntax of that style of using Zig quite cursed.

It seems too notation heavy and too clever, which discourages me from using it.

Racket has @-expr notation which seems similar but simpler. (Able to express html/xml like structure)

I think embedded DSLs like this are a bit too limited by what is valid Zig code, which makes me want to have custom string literals (because that would have better LSP integration potential than putting code in comptime strings) or write external DSLs instead (that have their own file extension and parser).

But all approaches seem to have their own downsides.

5 Likes

I don’t mean to be condescending, that wasn’t my intention, at all. Apologies if it comes across this way. I think it is more to do with my overall experience, with Javascript as a whole, and being burned by dependency hell, and constant updates to frameworks like React, who went from Class components to Functional components.

Once again apologies to @nurulhudaapon if I came across as condescending.

4 Likes

Thank you so much for all of your objections. Some suggestions make sense, and I guess for some I need to clarify. Let me clarify your mentioned question first.

  1. I do want to build a full production-grade framework. You can think of it as the alternative of Next.js (and JavaScript) → ZX (Zig) main for server-side rendered sites.

  2. So currently nothing here is client-side. Everything is rendered from the server. Zig/WASM to handle the whole stack or React/Svelte-like library for the client-side interactivity and still have Zig/WASM for performance-related tasks.

  3. Yes, that Go project seems similar. The problem I’m solving here is that with Next.js, if I build a server-side rendered site, the performance is just too slow. For example, I benchmarked with a simple page and ZX 120x, which is mostly because of Zig and some because of ZX. See the benchmark and some explanation here: https://youtu.be/6dVzIL0Oyb4

Now coming back to your first point.

You can write Zig, it’s just you can write HTML too and have Zig variables and other syntaxes like if/loop/switch within the HTML syntax to conditionally express your UI. So why not just use a template engine like Zine? Well Zine is not Zig, you write SuperHTML which has its own syntax not Zig. And also the DX is not that great to build complex sites with many components. And the second point, it transpiles ZX to Zig, you can directly write ZX but that would be the syntax you shared above where you are defining the UI with Zig, so it’s just giving you a better DX to write JSX-like syntax that gets transpiled to Zig during the build time. And then that Zig code doesn’t transpile to HTML that would void that whole point, instead from that tree it generates the HTML dynamically along with your dynamic content. So this is where the power of ZX comes in. And there are no steps involved here, you just set up ZX and write ZX (Zig but with HTML support within it) and run zig build serve and the site is up just like how JSX/React does it.

Let me share you an example:

The blog listing you are seeing here is being fetched from Hashnode upon your visit to the page: Nurul Huda (Apon)

And here is the code for it: (full code here)

pub fn Page(allocator: zx.Allocator) zx.Component {

    const posts = zigx_nuhu_dev.getPosts(allocator) catch |err| {
        return (<p>Error fetching posts. Please try again later. {[err:any]}</p>);
    };

    return (
        <main>
            <h1>Blog</h1>
            <ol>
            {for (posts) |post| {(
                <li>
                    <div>
                        <a href={post.url}><h3>{post.title}</h3></a>
                        <p>{post.brief}</p>
                    </div>
                </li>
            )}}
            </ol>
        </main>
    );
}

const zx = @import("zx");
const zigx_nuhu_dev = @import("zigx_nuhu_dev");

I know a lot of the contexts are missing as I was assuming everybody dealt with performance issue of Next.js and other web framework when it comes to SSR. They are slow because all the great ones are built using JavaScript and JSX end of the day so they tend to be slower.

And thanks again for taking the time to share your thoughts, that’s exactly why I shared this project here. Let me know if you have any other feedback or suggestions I would appreciate that.

5 Likes

Hey, I totally got your point and no apologies are needed, and yes, I mostly built that framework for the same issue I’m dealing with. Most of the good full-stack web frameworks are just too heavy and slow at server side rendering and there are just a lot of cases where we need fast server side rendered application and still want to have better DX unlike those templating engine that feels foreign to the actual programming language and expressing UI using Zig with too verbose syntax (those are not bad though for example Flutter or Swift UI is not that bad).

So at the end of the day I guess ZX → DX of JSX + Speed of Zig.

3 Likes

I prepared a video explainer mostly on why and how of ZX. But it is large haha sorry about that but I added chapters. :smile:

4 Likes

This is very cool, and I can see what you are doing here, offering an identical JSX like experience, but using zig.

I think this will be really appealing to devs who are very used to coding web apps the JSX way, but want to 100x the performance. Great work, I think this can go a long way.

A few observations & questions :

  • how you going to handle things like web components, where the element name is custom, so doesn’t exist in your enum of element types ? Could maybe provide a function to register custom elements or something like that. Or better, generate the enum when the initial transpiler does its pass.

  • really need an editor language server for zx now, lol

  • if your end point in the backend returns HTML (like using htmx or Datastar, etc) … I think it’s possible, by running the page() fn, getting back a component, then using component.render(writer) to generate the HTML output ? I think that will work as it is. Will give it a try and see.

  • only real criticism of this whole approach is that whilst it’s 100x faster than doing ssr on JavaScript, it still looks pretty busy doing extra work at runtime. Run the page() fn, generate a zx.Component, then step through the component struct and use render to convert it to HTML. It is what it is, but there is work and allocations and deallocations going on there to slow things down. Can’t be avoided.

It’s a balancing act, I know - using this zx approach gives a super nice DX, which makes development time & reading the app code much nicer, but at the cost of a little bit of extra runtime performance.

Would be interesting to bench different approaches and measure the overhead.

A dev can, for example, stretch fmt.print() to be a pretty decent templating engine up to a point, with no runtime parsing or allocations. The resulting code is definitely messier than a JSX like approach, and also not so friendly for convincing next.js users to try zig :slight_smile:

Keep going - this is going to be a fun project.

2 Likes

Thank you so much for the observations and questions.

  • Currently, zx.Component is a union type and has elements, text, and function (custom component); for web components, I would say we will just add another particular type for that.

  • Yes, I am working on it, basically just reusing ZLS and extending it. I hope to get it done soon.

  • Yes, exactly how you mentioned it, and the naming and function signature are currently exactly that. It just handles everything on its own.

  • Yeah, you are right, it is doing extra work for SSR. JSX/React does the same for SSR, but ZX just does it way faster. And yes, we will have some better way that lets you mark some components as static or have caching for some particular amount of time. We just store the rendered HTML in a global caching allocator and prevent the extra rendering work, and even have pre-rendered HTML for a complete static page. Something like the following:

pub fn Page(allocator: zx.Allocator) zx.Component {
   return (
       <main>
           ....
       </main>
   );
}

pub const config: zx.PageConfig = .{ .rendering = .ssg, .caching = .... };
const zx = @import("zx");

So much I’m exploring, haha. And again, I really appreciate the hustle of providing the observations, feedback, and criticism.

The idea would be to let the dev choose the approach depending on what they need, and we just pre-render at compile time, cache the rendered content, or do SSR.

Definitely, ZX will have the same thing when I properly add the SSG setup, so you run zig build export, and you have pure HTML. Yeah, exactly the idea is to bring more web people into using Zig with their existing familiarity, especially for performance-hungry server-side rendered sites like a product listing page on an e-commerce site.

Thank you for the encouragement, this have been a week working on it and I’m already impressed myself seeing the performance haha.

1 Like

ZX VSCode/Cursor Extension now has some initial LSP features! Basically planning to continue using ZLS and just injecting HTML+ZLS things for the ZX syntax.

It’s all ZLS with very little modification as of now, but already pretty useful.

2 Likes

Very cool! While I dislike JSX for reasons that will soon become clear, I’m a big fan of this kind of thing in strongly typed server-side languages like Zig or Haskell, as they provide type-safety and a familiar syntax without much runtime overhead.

I’ve seen mention in this thread of WASM, Svelte, and Next.js, and before you start going down the road of having Server components and client components, and some kind of state management features (if that is the direction you’re leaning), I would highly recommend checking out tools like htmx first.

Its a great 80/20 solution that let’s you build surpringly dynamic websites with very little Javascript or “state management”. When combined with Alpine.js and the alpine-morph extension, you can build all but the most dynamic websites (think stock trading, or online gaming), with almost no compromises.

That way, your library can focus solely on the rendering of HTML :slight_smile:

Sorry if it’s off topic, but htmx has changed my life as a web developer, and I can’t really imagine going back to a client-side focused framework, except for some edge cases like WebRtc where you simply can’t choose to do things server-side.

5 Likes

Yes, the pure goal is to be native for everything, native programming language (Zig in this case), and using all the native HTML features. And if you would like to include frontend libraries like HTMX, you can just do it and use that. ZX doesn’t need to know about this. I think already HTMX can be used with ZX. You just have a particular component that returns HTML based on passed query params, and you use HTMX to get that HTML.

For client-side things, I have already tried out having React components within ZX, and it just works without much, basically just a <div id="client-[hash]"></div>, and you attach a bundled (however you bundle it) JS of a React App that just renders to that particular div. ZX has no say here, but I would probably add something in the transpiler that autogenerates those divs when a React component import is detected and probably generate a JS file with a client-side component table having the generated id. Maybe all these are just plugins and outside of the scope of ZX. Still exploring all the ideas, but mainly focusing on getting things to work with just the native SSR rendering first. Everything else is just nice to have.

Definitely SSR and HTMX can get done on most sites! But yet there are highly interactive apps where you will want things like React. My initial goal was to build a React alternative for client-side rendering using Zig WASM so you can build fast client-side applications like Figma with the power of Zig. So maybe still do client-side rendering with Zig WASM in the future haha.

Thank you so much for your observation, by the way! I really appreciate all of these.

3 Likes

Put out a short video of the project over on YouTube. :smiley:

And ZX now has it’s own site: http://ziex.dev

And finally thePreformatted text VSCode/Cursor Extension now has auto formatting, which works great for most edge cases!
Adobe Express - Screen Recording 2025-11-10 at 9.08.04 PM-3

2 Likes

ZX CLI is available to install!

Linux/MacOS


curl -fsSL https://ziex.dev/install | bash

Windows


powershell -c "irm ziex.dev/install.ps1 | iex"

1 Like

React Component with ZX

I ended up adding support of importing React component has been implemented.

I also have static site generation implemented as well!