tmplx

Introduction

tmplx is a framework for building full-stack web applications using only Go and HTML. Its goal is to make building web apps simple, intuitive, and fun again. It significantly reduces cognitive load by:

  1. keeping frontend and backend logic close together
  2. providing reactive UI updates driven by Go variables
  3. requiring zero new syntax

Developing with tmplx feels like writing a more intuitive version of Go templates where the UI magically becomes reactive.

tmplx

You start by creating an HTML file. It can be a page or a reusable component, depending on where you place it.

You use the <script type="text/tmplx"> tag to embed Go code and make the page or component dynamic. tmplx uses a subset of Go syntax to provide reactive features like state, derived, and event handler. At the same time, because the script is valid Go, you can implement backend logic—such as database queries—directly in the template.

tmplx compiles the HTML templates and embedded Go code into Go functions that render the HTML on the server and generate HTTP handlers for interactive events. On each interaction, the current state is sent to the server, which computes updates and returns both new HTML and the updated state. The result is server-rendered pages with lightweight client-side swapping (similar to htmx). The interactivity plumbing is handled automatically by the tmplx compiler and runtime—you just implement the features.

Most modern web applications separate the frontend and backend into different languages and teams. tmplx eliminates this split by letting you build the entire interactive application in a single language—Go. With this approach, the mental effort needed to track how data flows from the source to the UI is reduced to a minimum. The fewer transformations you perform on your data, the fewer bugs you introduce.

Installing

tmplx requires Go 1.25 or later.

shell

This adds tmplx to your Go bin directory (usually $GOPATH/bin or $HOME/go/bin). Make sure that directory is in your PATH.

After installation, verify it works:

shell

Quick Start

Get a tmplx app running in minutes.

  1. Create a project

    shell
  2. Add your first page (pages/index.html)

    tmplx
  3. Generate the Go code

    shell
  4. Create main.go to serve the app

    go
  5. Run the server

    shell

That's it! Open http://localhost:8080 and you now have a working interactive counter.

Pages and Routing

A page is a standalone HTML file that has its own URL in your web app.

All pages are placed in the pages directory. Default pages location is ./pages. Change it with the -pages-dir flag:

shell

tmplx uses filesystem-based routing. The route for a page is the relative path of the HTML file inside the pages directory, without the .html extension. For example:

When the file is named index.html, it serves its directory's URL, which ends with a trailing slash. The page matches that URL exactly — it does not catch other paths under the directory — and a request without the trailing slash is redirected to it.

A name and a directory are different URLs: login.html serves /login while login/index.html serves /login/, and both can exist in the same project. Two files that derive the same route cause compilation failure.

The exact match is what {$} does: in Go's net/http.ServeMux, a pattern ending in / matches the whole subtree, so a bare /docs/ pattern would serve the index page for every unmatched URL under it. tmplx therefore registers every index.html with {$} appended: pages/docs/index.html becomes the pattern GET /docs/{$}, which matches only /docs/ itself. That string is the page's identity, so you will meet it wherever the page is referred to — in the Routes() patterns when you attach middleware, and in the event POST URLs in the network tab:

To add URL parameters (path wildcards), use curly braces in directory or file names inside the pages directory. The name inside must be a valid Go identifier.

These patterns are compatible with Go's net/http.ServeMux (Go 1.22+). The parameter values are available in page initialisation through tx:path comments.

A {name...} wildcard matches the rest of the URL, slashes included — a catch-all. It serves every path under its directory that no more specific page matches: an exact page wins first, then the directory's index.html, then the catch-all.

tmplx compiles all pages into a single Go file you can import into your Go project. The pages directory can be outside your project, but keeping it inside is recommended.

tmplx Script

<script type="text/tmplx"> is a special tag that you can add to your page or component to declare state, derived, event handlers, functions, and the special init() function to control your UI or add backend logic.

Each page or component file can have exactly one tmplx script. Multiple scripts cause a compilation error.

In pages, place it anywhere inside <head> or <body>.

tmplx

In components, place it at the root level.

tmplx

Imports

The script is plain Go, so you pull in packages with normal Go import declarations—single or grouped—at the top of the block.

Imports resolve against your project's go.mod. tmplx walks up from the working directory to the nearest go.mod (see CLI) and type-checks the script against that module, so you can import:

An import that does not resolve fails compilation with cannot import <path>. Imported struct or named types can be used as state. The playground resolves the standard library only.

Reserved Names

The compiler reserves two naming patterns for its own use:

Expression Interpolation

Use curly braces {} to insert Go expressions into HTML. Expressions are allowed only in:

Placing expressions anywhere else causes a parsing error.

tmplx converts expression results to strings using fmt.Sprint. The output is HTML-escaped in both text nodes and attribute values to prevent cross-site scripting (XSS)—an interpolated value cannot inject markup or break out of its attribute.

Expressions run on the server every time the page loads or a component re-renders after an event. Avoid side effects in expressions, such as database queries or heavy computations, because they execute on every render.

html
html

Add the tx-ignore attribute to an element to disable expression interpolation in that element's attributes and its direct text children. Descendant elements are still processed normally.

html
html

State

State is the mutable data that describes a component's current condition.

Declaring state works like declaring variables in Go's package scope. If you provide no initial value, the state starts with the zero value for its type.

tmplx

To set an initial value, use the = operator.

tmplx

Although the syntax follows valid Go code, these rules apply:

  1. Only one identifier per declaration.
  2. The type must be JSON-compatible.

The 1st rule is enforced by the compiler. General JSON-compatibility is not checked at compile time (for now)—an interface or channel type compiles and then fails at runtime. The one exception is a function-typed state, which is rejected at compile time; use a callback prop for a function input.

Some invalid state declarations:

tmplx

Some valid state declarations:

tmplx

State can hold any JSON-compatible Go type. A few of them, live:

  • int: 3
  • string: tmplx
  • bool: true
  • slice: 2 items
types.html

The tmplx script cannot contain Go type declarations. To use your own struct or named types as state, declare them in a regular Go package and import them—then reference the imported type in the var declaration.

Derived

A derived is a read-only value that is automatically calculated from states. It updates whenever those states change.

Declaring a derived works the same way as declaring package-level variables in Go. When the right-hand side of the declaration references existing state or other derived values, it is treated as a derived value.

Derived values follow most of the same rules as regular state variables, but with some differences:

  1. Only one identifier per declaration.
  2. Derived values cannot be modified directly in event handlers, though they may be read.

count = 1, doubled = 2

derived.html

A derived can read any number of states—here a slice is joined into a class string:

tmplx

Event Handlers

An event handler binds a DOM event to Go code that runs on the server. Add an attribute that starts with tx-on followed by the event name (tx-onclick, tx-oninput, tx-onchange, …); its value is a body of Go statements.

The body is plain Go—mutate state directly, call a function, or both. The simplest handler just assigns to a state variable; no function is required.

0
counter.html

Any valid Go statement works in the body—here a compound assignment doubles a value on each click:

1

double.html

Event Properties

For events fired on a form control, you can read values directly from the DOM event object inside the handler body. The runtime injects them into the request automatically.

You typed: (0 chars)

inputlive.html

Functions

A function is a standalone, reusable Go function declared in the tmplx script as func name(...) { ... }. It is a separate idea from an event handler: a handler is a tx-on* binding, while a function is plain logic you can call from many places—an event handler, a form, init(), another function, or an expression.

Functions are not one-to-one with events. One function can back many bindings, and a binding may call no function at all (inline statements, as above) or several. Only tx-on* and tx-action bindings compile to HTTP endpoints—a function on its own does not.

Functions take parameters and may return values. Below, a single addNum backs ten buttons; each click passes a different argument:

0

addn.html

A state-mutating function runs on the server through the handler or form that calls it. A function used inside an expression or derived value runs on every render, so keep functions in that position pure—no state mutation or write-side effects.

Captured Locals

Any local variable bound by an enclosing tx-for init clause, tx-for range form, or tx-if/ tx-else-if init form is automatically captured by handlers in the subtree. Just reference the local by name; the framework figures out which values cross the wire and decodes them with their inferred Go type.

In the addn example above, i is captured from the tx-for init clause and decoded as int on the server. Captures from tx-if/tx-else-if init forms work the same way:

tmplx

Both f (from tx-for) and n (from the tx-if init) are captured automatically.

init()

init() is a special function that runs automatically the first time a page or component is rendered. For pages, it runs on every GET request. For components, it runs when the component has no saved state yet (for example, the first time it appears on the page, or the first time a new tx-for iteration produces it). After that, subsequent renders reuse the saved state and skip init().

2026-07-14T09:34:27Z

current-time.html

Another common use case is to initialize one state from another state without turning the second variable into a derived state.

tmplx

Path Parameters

When a page route contains a wildcard (see Pages and Routing), you can pull the captured value into a state variable by annotating the declaration with a //tx:path comment.

Rules:

The captured value is assigned before init() runs, so init() can use it to populate other state (for example, by loading a record from the database).

Single parameter. For a route pages/blog/post/{post_id}.html:

tmplx

Multiple parameters. Each wildcard gets its own declaration. For a route pages/blog/{year}/{slug}.html:

tmplx

After initialization, the variable behaves like any other state: it's serialized, sent to the server on events, and can be reassigned from handlers (though reassigning it does not change the URL).

Try it live: /hello/world · /hello/tmplx. This page binds the {name} URL segment with //tx:path name:

pages/hello/{name}.html

Control Flow

tmplx avoids new custom syntax for conditionals and loops. It embeds control flow directly into HTML attributes, similar to Vue.js and Alpine.js.

Conditionals

To conditionally render elements, use the tx-if, tx-else-if, and tx-else attributes on the desired tags. The values for tx-if and tx-else-if can be any valid Go expression that would fit in an if or else if statement. The tx-else attribute needs no value.

red

cond.html

You can declare local variables and handle errors exactly as you would in regular Go code. Local variables declared in conditionals are available to the element and its descendants, just like in Go.

html

A conditional group consists of consecutive sibling nodes that share the same parent. Disconnected nodes are not treated as part of the same group. A standalone tx-else-if or tx-else without a preceding tx-if will cause a compilation error.

Loops

To repeat elements, use the tx-for attribute. Its value can be any valid Go for statement, including classic for or range for.

Local variables declared in the loop are available to the element and all of its descendants, just like in Go.

Always add a tx-key attribute with a unique value for each item. This gives the compiler a unique identifier for the node during updates.

5
____ *
___ ***
__ *****
_ *******
*********
triangle.html
html

To branch per item, do not put tx-if and tx-for on the same element—the condition is compiled outside the loop and cannot see the loop variable, which is a compile error. Put the conditional on an element inside the loop so it can read the bound variable:

  • 1 (small)
  • 2 (small)
  • 3 (small)
  • 4 (big)
  • 5 (big)
  • 6 (big)
condrows.html

<template>

The <template> tag is a non-rendering container that lets you apply control flow attributes (tx-if, tx-else-if, tx-else, or tx-for) to a group of elements at once.

The <template> itself is removed from the output; only its children are rendered (or not, depending on the control flow).

You can nest <template> tags and combine them with other control flow attributes on child elements.

tmplx
tmplx

Forms

Attach a handler to a <form> with tx-action. When the form is submitted, tmplx cancels the default submission, collects every named form element, and calls the handler on the server.

The value of tx-action must be the name of a function declared in the tmplx script. Each form element's name attribute must match a parameter name on that function; unnamed elements are ignored.

greeting.html

Values are JSON-decoded into each parameter's Go type, so the parameter type is what determines how the string is parsed. The runtime serializes form elements by input type:

Because submission goes through a full server round-trip, use native HTML validation (required, minlength, pattern, ...) to catch client-side errors before the request is sent. For richer live-updating inputs, combine tmplx with a client-side library like Alpine.js.

Component

Components are reusable UI building blocks that encapsulate HTML, state, and behavior.

Create a component by placing an .html file in the components directory (default: ./components). tmplx automatically registers it as a custom element with the tag name tx- followed by the relative path (without the .html extension), with directory separators replaced by -.

Filenames and directory names may contain only a-z, 0-9, -, and _. Uppercase letters are rejected.

Examples:

Components can contain their own <script type="text/tmplx"> for local state and logic, and can be used in pages or nested inside other components.

Props

Props are inputs the parent passes to a child component. Inside the child, a prop is declared like a state variable, but with a //tx:prop doc comment.

tmplx

Rules:

Passing props

Prop attribute values on the parent are parsed as Go expressions, not as plain strings. Pass a literal by writing the literal directly; pass a parent variable by its name.

html

The expression is re-evaluated whenever the parent re-renders, so the child stays in sync with the parent's state automatically.

Clicks: 0

Doubled: 0

stat.html (the component)
props.html (using it)

Callback Props

A callback prop lets a child notify the parent when something happens. It is just a prop whose type is a function: declare it with //tx:prop and a function type. With no default the parent must supply an implementation (a required prop); give it a function-literal default to make the parent override optional.

In the child, call it from a handler the same way you call a function:

tmplx

In the parent, pass the bare name of a tmplx-script function as the attribute whose key matches the child's prop name:

tmplx

When the child calls onselect(42), the parent's pick runs on the server with that argument and the parent re-renders. A callback call can be mixed freely with other statements in the same handler—for example tx-onclick="count++; onselect(42)".

To make the override optional, give the prop a function-literal default. The parent may then omit the attribute and the child falls back to its own implementation:

tmplx

A runnable version: <tx-incbtn> takes a label and an onpress callback; the parent passes its own add function and counts the presses.

total: 0

incbtn.html (the component)
callback.html (using it)

<slot>

A <slot> marks a place in a component's template where the parent can inject content. Slots are how components stay composable: the child decides the shape, the parent fills in the details.

Declaring slots in a component

Each slot is either the default slot (no name) or a named slot. A component may have at most one default slot, and named slots must be unique. Slots cannot be nested inside other slots.

html

Content placed inside <slot>...</slot> is fallback content—it renders only when the parent does not fill that slot.

Filling slots from the parent

Put fill content directly inside the component tag. Use the slot attribute on a child element to target a named slot; everything else becomes the default fill.

html

Only the direct children of the component tag are considered when matching slots—a slot attribute on a deeply nested element has no effect.

Card title

Any markup here fills the <slot>.

slotcard.html (the component)
slotdemo.html (using it)

Scope: fills use the parent's state

This is the most important rule. The content you pass into a slot is still parent code: expressions, event handlers, and directives inside a fill see the parent's state, derived, and prop variables—not the child's.

tmplx

Here user and logout are defined on the page that uses <tx-card>, not inside the card component. When the button is clicked the page's handler runs and the fill re-renders against the page's updated state.

Live example

The docs site uses a simple <tx-example-wrapper> component with a single default slot to frame every live demo on this page. The component is just:

example-wrapper.html

And callers wrap any demo with it:

html

CLI

Running tmplx inside any directory of your Go module walks up to the nearest go.mod and uses that as the project root. All path flags default relative to that root.

Flag Default Description
-pages-dir ./pages Directory containing pages.
-components-dir ./components Directory containing reusable components.
-output-file ./routes.go Path to the generated Go file.
-package-name main Package name for the generated Go code.
-handler-prefix /tx/ URL path prefix for generated event handler routes.

Syntax Highlight

Neovim Plugin