GoX is a Go language extension that lets you write HTML-like templates as typed expressions that compile to plain Go.
func Hello(name: string) gox.Elem {
return <h1>
Hello ~(name)!
</h1>
}
// or
elem Hello(name: string) {
<h1>
Hello ~(name)!
</h1>
}
- Seamless editor support: near-native language-server experience across
.goxand regular.gofiles. - Full templating toolbox: conditionals, loops, composition, and reusable components.
- Extensible rendering pipeline: templates compile to a stream of render jobs you can process with custom printers.
templcompatible:gox.Elemrenders viaRender(ctx, w)and can be used where atemplcomponent is expected.
The editor extension is enough to get started. If you want the CLI tool or prefer a manual setup:
- Download the
goxbinary from GitHub Releases and add it to yourPATH(or build it from source). - Install the Tree-sitter grammar: https://github.com/doors-dev/tree-sitter-gox
The editor extensions configure everything automatically. See their documentation for available options.
For manual setup:
- Attach the GoX language server to both
.goand.goxbuffers (seegox srv -helpfor available options). - Disable the default Go language server (
gopls). GoX starts its owngoplsinstance and proxies features while adding.goxsupport. - Ensure
goplsis on yourPATH, or configure a custom path forgox.
go get github.com/doors-dev/goxKeep the tooling version and Go package version in sync. Currently, the language server does not enforce this automatically.
Extended syntax is available in .gox files.
The GoX language server compiles .gox files into generated .x.go files and keeps them up to date whenever you save a .gox file.
- If an existing
.x.gofile was generated by a newer version of GoX, the server will report an error until you upgrade.- Orphaned
.x.gofiles are deleted automatically. Avoid naming regular files with the.x.gosuffix.- Do not edit
.x.gofiles manually.
You can also trigger generation manually using the CLI:
gox gen [flags]
-force
overwrite existing files without checking
-no-ignore
ignore .gitignore
-path string
file or directory to generate (default ".")
gox.Elem is the fundamental renderable type:
- Conceptually: a function that emits output through a cursor (
type Elem func(cur Cursor) error) - Practical: a value you can return, pass around, compose, and render.
Elem also:
- implements
gox.Comp(so an element can be used as a component) - renders through
Render(ctx, w)fortemplcompatibility - can be rendered through
Print(ctx, printer)for custom pipelines
At runtime, GoX renders by streaming jobs into a printer:
- A Job is a unit of work: it carries a
context.Contextand anOutput(w)method. - A Printer consumes jobs. The default printer writes output sequentially to an
io.Writer, and stops early ifjob.Context().Err()is non-nil (canceled/deadline exceeded).
This architecture enables custom printers for preprocessing, analysis, instrumentation, or alternative rendering strategies.
An element is an HTML tag expression:
el := <div></div>
You can return it, pass it as a function argument, store it in a struct field, etc.
func Page(content gox.Elem) gox.Elem {
return <html>
<body>
~(content)
</body>
</html>
}
// call
Page(<h1>Hello!</h1>)
You can also use an anonymous wrapper tag (fragment) to group content without adding an actual HTML element:
<>Hello</>
elem is a shorthand for functions that return gox.Elem:
elem Page(content gox.Elem) {
<html>
<body>
~(content)
</body>
</html>
}
Anonymous functions are also supported:
page := elem(content gox.Elem) {
<html>
<body>
~(content)
</body>
</html>
}
…and methods:
type Component struct {
content gox.Elem
}
elem (c *Component) Main() {
<html>
<body>
~(c.content)
</body>
</html>
}
ctxis available inside templates and provides acontext.Context.- Self-closing syntax works for any tag. For non-void tags, GoX will automatically emit a closing tag (
<div/>→<div></div>).
A component is anything that implements gox.Comp:
type Comp interface {
Main() gox.Elem
}For compatibility,
gox.Elemalso implementsgox.Comp.
Insert values using a placeholder: ~(value):
<h1>Hello ~(name)!</h1>
You can also insert multiple values:
<h1>Hello ~(name, " ", surname)!</h1>
Values are rendered using default formatting, with special handling for:
gox.Comp/gox.Elemtempl.Component(fromgithub.com/a-h/templ)string,[]string[]gox.Comp,[]gox.Elem,[]any(rendered item-by-item)gox.Job,[]gox.Jobgox.Editor
For simple literals you can omit the parentheses. Strings, numbers, composite literals (struct/array/slice/map):
~// render user card component ~UserCard{ Id: id, }
gox.Job writes directly to the output stream:
type Job interface {
Context() context.Context
Output(w io.Writer) error
}gox.Editor provides low-level access to the rendering cursor:
type Editor interface {
Edit(cur Cursor) error
}If / else-if / else are available as expressions:
<div>
~(if user != nil {
Hello ~(user.name)!
} else if loggedOut {
Bye!
} else {
Please log in
})
</div>
Loops:
~(for _, user := range users {
<tr>
<td>~(user.name)</td>
<td>~(user.email)</td>
</tr>
})
Use parentheses to provide a Go expression as an attribute value:
elem block(id: string, content: any) {
<div id=(id)>~(content)</div>
}
- Any value is accepted.
- If the value is
falseornil, the attribute is omitted. - Attribute names are case-sensitive (
classandClassare different attributes). Always use consistent casing.
During rendering, the default formatter is used unless the value implements gox.Output:
type Output interface {
Output(w io.Writer) error
}To compute a new attribute value from the previous one, implement gox.Mutate:
type Mutate interface {
Mutate(prev any) (new any)
}To inspect or modify all attributes of an element right before it’s rendered, implement gox.AttrMod:
type AttrMod interface {
Modify(ctx context.Context, tag string, attrs Attrs) error
}Attribute modifiers are applied at render time and can mutate the full attribute set. To attach one, place it in parentheses inside the opening tag:
<button (LandingAction)>
Request Demo!
</button>
You can attach multiple modifiers as a comma-separated list:
<button (TrackClick, LandingAction)>
Request Demo!
</button>
An inline expression is a function literal evaluated immediately during rendering. Its return value is inserted exactly where it appears (node content or attribute value).
<div>
~func {
user, err := Users.get(id)
if err != nil {
return <span>DB error</span>
}
return <div>~(Card(user))</div>
}
</div>
In attributes:
<input type="checkbox" checked=func {
user := Users.get(id)
return user.Agreed // false or nil omits the attribute
}>
To run Go code during rendering (without rendering output), use ~{ ... }:
~{
user := Users.Get(id)
}
<div>~(user.name)</div>
To comment inside templates, use ~// or ~/* ... */:
~// <div></div> - commented out
HTML comments are also supported.
To output HTML verbatim (without escaping or template processing), wrap it in the special raw tag: <:>...</:>.
<:>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="..." />
</svg>
</:>
Recommended for large static fragments—especially inline SVG—to reduce rendering overhead.
A proxy captures an element subtree and can transform it before rendering. Apply a proxy by prefixing the target expression with ~>:
~>(proxy) <div>
Proxy can apply transformations to this HTML.
</div>
A proxy must implement:
type Proxy interface {
Proxy(cur Cursor, elem Elem) error
}You can apply multiple proxies as a comma-separated list:
~>(Track, Minify, Instrument) <div>
Proxy can apply transformations to this HTML.
</div>
Proxies can reduce boilerplate, add integrations, or collect analytics by wrapping/replacing subtree rendering.
gox.Elem can be rendered directly to an io.Writer:
err := elem.Render(ctx, w)To preprocess output, analyze it, or build custom backends, implement a printer:
type Printer interface {
Send(j Job) error
}Jobs produced by GoX are pooled for performance. The default job types release themselves back to an internal pool after
Output(...)completes.
Send receives values through the gox.Job interface, but the jobs produced by GoX have concrete types (e.g. *JobHeadOpen, *JobHeadClose, *JobText, *JobRaw, *JobBytes, *JobComp, *JobTempl, *JobFprint, *JobError).
A custom printer can type-switch on the incoming job value to implement transforms, instrumentation, buffering, routing, or alternative output strategies:
func (p *MyPrinter) Send(j gox.Job) error {
switch v := j.(type) {
case *gox.JobHeadOpen:
// inspect v.Tag / v.Kind / v.Attrs, record metrics, rewrite attrs, etc.
case *gox.JobText:
// observe/transform text
case *gox.JobComp:
// Component boundary:
// - trace/measure per component, or
// - render the component with this printer to intercept its internal jobs:
// if el := v.Comp.Main(); el != nil { return el.Print(v.Ctx, p) }
// - or render it elsewhere (buffer/worker) and merge output in order.
}
return j.Output(p.w) // or route to another sink
}Notes:
- Treat incoming job objects as single-use: do not store them, do not reuse them, and do not keep references to their fields beyond the scope of
Send. - The default
NewPrinterimplementation checksj.Context().Err()before callingj.Output(...). Custom printers should apply their own cancellation policy if needed. - The open and close jobs for the same head share the same
ID(useful for pairing and tracing). - Container heads produce no HTML output, but still emit open/close jobs.
- Element “open” and “close” jobs for the same head share the same ID.
- “Container” heads exist for grouping in the job stream and produce no HTML output, but still have open/close jobs.
Disclaimer: GoX is an independent, third-party project and is not affiliated with, endorsed by, or sponsored by The Go Project, Google, or any official Go tooling.
