Skip to content

What is alacris-go?

The example app: a live Kanban board. Cards keep the same DOM nodes as they move; the server writes props, not HTML.

alacris-go renders alacris web components from Go and templ. It is three layers plus an optional desktop host, each usable on its own.

A component’s shadow content is produced by its setup() function when the element connects, in the browser. There is no server-side rendering of component internals, and alacris does not support declarative shadow DOM.

So what a Go server renders is the element itself:

What the server produces
return alacris.E("info-card").
Prop("tone", "warn").
Slot("title", text("Ada Lovelace")).
SlotAs("p", "body", text("Wrote the first algorithm."))
renders verified by go test
<info-card tone="warn">
<div slot="title">Ada Lovelace</div>
<p slot="body">Wrote the first algorithm.</p>
</info-card>
which the browser turns into the real runtime, upgrading that markup
Ada Lovelace

Wrote the first algorithm.

Slot content is light DOM, so it is in the first paint.

Two consequences:

  • Slot content is real HTML in the document. It is in the first paint and in a crawler’s view of the page. Put content that matters for SEO, or for readers with JavaScript disabled, here.
  • Everything inside the shadow root appears when the module loads. Between first paint and that moment the element is present but empty. alacris.Pending hides it until then.

alacris coerces an attribute using the type of the prop’s default in define(). Look at what that means for an object default:

define.js
const coerce = (v, d) => {
if (v === null) return typeof d === 'boolean' ? false : d;
const t = typeof d;
if (t === 'number') return +v;
if (t === 'boolean') return v !== 'false';
if (d !== null && t === 'object') { try { return JSON.parse(v); } catch { return d; } }
return v;
};

JSON.parse is the object path. Objects and arrays round-trip through attributes, so a fully-formed component needs no post-load property assignment and no hydration payload:

A struct, a map, a time: all attributes
type point struct {
X int `json:"x"`
Y int `json:"y"`
}
return alacris.E("x-demo").
Prop("tags", []string{"a", "b"}).
Prop("origin", point{X: 1, Y: 2}).
Prop("lookup", map[string]int{"b": 2, "a": 1}).
Prop("at", time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC)).
Prop("timeout", 1500*time.Millisecond)
renders verified by go test
<x-demo
tags="[&#34;a&#34;,&#34;b&#34;]"
origin="{&#34;x&#34;:1,&#34;y&#34;:2}"
lookup="{&#34;a&#34;:1,&#34;b&#34;:2}"
at="2026-08-08T12:00:00Z"
timeout="1500"
></x-demo>
which the browser turns into the real runtime, upgrading that markup

Objects and arrays cross as JSON, so no post-load assignment is needed.

The page is complete before any JavaScript has run.

LayerWhat it doesWhat it costs
alacrisRender elements, serve the runtime.Nothing. Ordinary request/response.
gen + CLITyped Go wrappers from your define() calls.A generate step, next to templ’s.
liveServer-driven props and component events.A stateful server and session affinity.
appThe same live handler, in an OS webview.CGO, -tags desktop, a nested module.

Most applications want the first two. Use the third when the server owns what the page shows (a shared board, a dashboard, a queue) and you would otherwise be writing polling or a custom WebSocket protocol. Use the fourth when that same process should be a window instead of a URL.

The module’s example is the third layer in a page, and the fourth with a flag: examples/todo is a live Kanban board whose chrome is Alacris UI and whose state lives on the server.

Terminal window
go run ./examples/todo
go run -tags desktop ./examples/todo -desktop
  • Not a component framework. Component internals are JavaScript, because setup() runs in a browser. alacris-go does not invent a Go DSL for templates and signals.
  • Not a replacement for htmx or Datastar. Those swap HTML. This writes properties, which is a smaller and more precise operation, but only works for state a component already models as a prop.
  • Not tied to the live layer. The first two layers are stateless and have no dependency on the third.
  • Not Wails, not Fyne, not Tauri’s JS bindings. The desktop host reuses live.On and Handle.Set. Native dialogs and menus are Go.