Why I built gsx instead of just templ

jackielii1 pts0 comments

Why I built gsx · jackieli.dev<br>I have been building gsx, a Go template compiler<br>with JSX-style markup, component declarations, and plain Go output.<br>The obvious question is: why?<br>More specifically: why not just use templ?<br>Firstly, I like templ. gsx is not built because I think templ is bad. In fact,<br>gsx is intentionally compatible with the templ ecosystem. A gsx.Node has the<br>same render shape as templ.Component:<br>Render(ctx context.Context, w io.Writer) error

So this is not a &ldquo;throw everything away and start again&rdquo; situation.<br>The difference is the tradeoff.<br>templ tries quite hard to stay simple. The syntax is close to Go, the compiler<br>model is relatively simple, and many things are explicit. That is a very<br>reasonable design.<br>But I think it pays for that simplicity with ergonomics.<br>The recurring pressure

Link to heading<br>After using templ, I kept running into the same category of problems.<br>Not correctness problems necessarily. More like friction.<br>Things like:<br>component calls do not read like HTML<br>passing rich inline markup often needs extra named components<br>class composition can become verbose<br>JavaScript/data interpolation is awkward<br>JSON data islands need helper code<br>attributes want to behave more like markup attributes<br>editor/dev-loop support needs to understand the template language deeply<br>What convinced me this was not just my personal taste is that templ has had many<br>issues and proposals in exactly these areas.<br>There are proposals for HTML-style component authoring by me: #663, #1181.<br>There is a proposal for anonymous/inline templ functions by others: #1150.<br>There are discussions around passing Go data into JavaScript: #944, #838.<br>There was a proposal for JSON helpers: #739.<br>There is even a newer discussion about whether interpolating Go data inside tags is worth the parser complexity: #1408.<br>Class and attribute ergonomics show up too: #61, #902, #933.<br>And dev-loop/tooling pressure is also there: #318.<br>So I think the demand is real. People like Go-checked templates, but they also<br>want the authoring experience to feel more like writing HTML.<br>Retrofitting is hard

Link to heading<br>The hard part is that these features are not isolated.<br>HTML-style component calls are not just syntax. Once you have:<br>Card title="Hello" class="featured" /><br>the compiler needs to know whether Card is a component, what props it accepts,<br>how attributes map to fields, what type each expression has, and what context<br>each value is rendered into.<br>If you want good editor support, the language server needs the same knowledge.<br>If you want safe JavaScript interpolation, the compiler needs to understand<br>whether the value is in a JS value position, a string position, a regex position,<br>an attribute value, or a JSON data island.<br>If you want class merging, the compiler needs to treat class as a structured<br>value, not just a string attribute.<br>You can add these things one at a time, but at some point you are no longer<br>keeping the compiler simple. You are building a more ambitious toolchain.<br>That is the point where I thought: maybe the design should start there.<br>The gsx tradeoff

Link to heading<br>gsx spends more complexity in the toolchain to make templates nicer to write.<br>It uses go/packages and go/types to scan and analyze real Go source. This is<br>not free. It is more machinery than a simpler parser/code generator.<br>But it buys useful things.<br>A component call can look like this:<br>component Greeting(name string) { p {attrs...}>Hi { name }p> }<br>component Card(title string, n int) { div>{ title }: { n }div> }<br>component Panel(title string) { section>{ title }section> }

component Page() {<br><><br>Greeting name="Ann" class="friendly"/><br>Card title="T" n={2}/><br>Panel title="P"/>

The call site reads like markup.<br>The data is still Go.<br>The generated output is still plain Go.<br>Class ergonomics

Link to heading<br>Class composition is one of those small things that matters a lot in real UI<br>code.<br>In gsx:<br>span class={ "tag", "tag--active": active }><br>{ label }<br>/span><br>This renders as:<br>span class="tag tag--active">stablespan>

There is also explicit attribute forwarding:<br>component Button(variant string) {<br>button class="btn" data-variant={variant} { attrs... }><br>{ children }<br>button><br>component Page() {<br>Button variant="primary" class="w-full" data-test="x" hx-post="/go"><br>Save<br>Button><br>The rendered class is merged:<br>button class="btn w-full" data-variant="primary" data-test="x" hx-post="/go"><br>Save<br>button>

This is a very boring feature, but boring features are where UI ergonomics live.<br>JavaScript, but not the whole circus

Link to heading<br>I do not hate JavaScript.<br>I have used enough JavaScript and Node.js to know the bad parts, but I also do<br>not want to pretend the good parts are bad.<br>JSX ergonomics, fast reloads, editor tooling, browser error overlays, and tight<br>feedback loops are good ideas.<br>I just do not want the whole circus.<br>In gsx, JavaScript-valued attributes are explicit:<br>button @click=js`openMenu()`>Open/button><br>For Alpine-style attributes:<br>div x-data=js`{ open:...

class component data templ like button

Related Articles