Skip to content

Getting Started

This page takes you from an empty directory to a window with a working counter, then shows how to render the same screen without a window.

  • Go 1.27 or newer.
  • To open a window: a Wayland or X11 session on Linux, or Windows. macOS builds, but has no window backend yet.
  • Nothing else. There is no C toolchain, no GTK, no browser: build with CGO_ENABLED=0 and ship one static binary.

The tela command can do this and the next step for you: tela new counter (see CLI). By hand:

Terminal window
mkdir counter && cd counter
go mod init example.com/counter
go get github.com/kuronosu/tela@latest

A Tela program is a tree of nodes built by components: functions, like the one below, and structs with a Build method (next section). The elements (Div, Button, Text…) live in package h, which is meant to be dot-imported so markup reads like JSX; hooks, events and Run live in package tela.

package main
import (
"github.com/kuronosu/tela"
. "github.com/kuronosu/tela/h"
)
// Counter shows a count and a button that adds one. It is the public name:
// tela.C gives the function below a component boundary of its own, so
// whoever uses Counter() never writes tela.C.
func Counter() tela.Node { return tela.C(counter) }
func counter() tela.Node {
count, setCount := tela.UseState(0)
return Div(Class("flex flex-col items-center gap-6 p-10 rounded-3xl "+
"bg-linear-to-br from-indigo-500 to-fuchsia-600 shadow-2xl"),
H1(Class("text-5xl font-bold text-white tracking-tight"),
Textf("Count: %d", count)),
Button(Class("px-6 py-3 rounded-xl bg-white/15 text-white "+
"hover:bg-white/25 active:scale-95"),
OnClick(func(tela.MouseEvent) { setCount.Set(count + 1) }),
Text("Increment"),
),
)
}
// Preview lets `tela snapshot` and `tela inspect` render the screen.
func Preview() tela.Node { return Counter() }
func main() {
tela.Main(tela.App{
Title: "Counter",
Width: 480,
Height: 320,
RootClass: "bg-slate-100",
Root: Counter(),
})
}

The pieces:

  • tela.UseState returns the state and a tela.Setter. setCount.Set(v) replaces the state and rebuilds the component in the next frame; setCount.Update(func(n int) int { return n + 1 }) computes it from the previous state.
  • Class takes Tailwind v4 classes. Variants such as hover: and active: restyle the button under the pointer.
  • OnClick receives a tela.MouseEvent by value.
  • tela.App describes the window and tela.Main opens it and runs the event loop until it is closed. When no display is available it prints the error and exits with status 1 (tela.Run returns the error instead). Both panic when Root is nil.
  • Counter is a function component. Its hooks live in the private counter, and the public Counter() returns tela.C(counter): the component boundary is written once, where the component is defined, so Preview, Root and the tests just call Counter(). Calling counter() directly would run its hooks inside the caller, and dev mode stops you with the fix. A struct component needs no wrapper.

A component that takes props is a struct with a Build method. Its fields are the props, its children are a field of type tela.Component, and every value you put in a tree is a component of its own, with its own state:

// Stat is a labeled number with a "details" toggle.
type Stat struct {
Label string
Value int
Children tela.Component // shown when open; several nodes via Fragment
}
func (s Stat) Build() tela.Node {
open, setOpen := tela.UseState(false)
return Div(Class("rounded-xl bg-white p-4 shadow"),
P(Class("text-sm text-slate-500"), Text(s.Label)),
P(Class("text-3xl font-semibold"), Textf("%d", s.Value)),
Button(OnClick(func(tela.MouseEvent) { setOpen.Set(!open) }), Text("Details")),
If(open, s.Children))
}
func Dashboard() tela.Node {
return Div(Class("grid grid-cols-2 gap-4 p-6"),
Stat{Label: "Visitors", Value: 1284, Children: Text("+12% this week")},
Stat{Label: "Signups", Value: 87})
}

Build has a value receiver. When Dashboard builds again and a Stat’s fields are equal to the previous ones, that Stat is not built again. See Hooks for keys, skipping and function components.

Terminal window
CGO_ENABLED=0 go run .

The counter rendered by Tela

Because the whole pipeline is headless, Tela can draw a screen to a PNG and print what it contains as text. That is the loop an agent (or you, in a terminal) uses to check a UI: write, snapshot, look, inspect, fix. The tela command is in the module; install it once and use it from any project:

Terminal window
CGO_ENABLED=0 go install github.com/kuronosu/tela/cmd/tela@latest
tela snapshot --size 480x320 -o counter.png .
tela inspect --size 480x320 .

(go run github.com/kuronosu/tela/cmd/tela snapshot ... does the same without installing anything.) While you edit, tela dev . keeps the window open and reloads it on every save, with its state.

- 'heading "Count: 0" [level=1]'
- button "Increment"

The commands render the package’s Preview function (or a tela.Previews variable with several named screens, each started in the state you give it), in the window of its package-level var App when it has one. See CLI.

telatest.Mount runs the same tree in a headless window and drives it with real pointer and key events:

package main
import (
"testing"
"github.com/kuronosu/tela/telatest"
)
func TestIncrement(t *testing.T) {
app := telatest.Mount(t, Counter(), telatest.Size(480, 320))
app.Click(telatest.ByRole("button", "Increment"))
app.ExpectText("Count: 1")
}

See Testing for locators, expectations, goldens and the manual clock.

While you develop, Tela reports the mistakes that compile, with the fix. Dev mode is on in every development build (go run ., go build in a source checkout) and in every telatest test, and off in release builds (built with -trimpath, or installed with go install pkg@version). App.Dev overrides that, and TELA_DEV=1 or TELA_DEV=0 overrides both.

  • An unknown class is printed with the closest one (unknown class "rounded-xxl"; did you mean "rounded-2xl"?); in a test it fails the test.
  • An unknown role, input type or easing, a change in the order of hooks, a hook called outside its component (counter() where Counter() belongs, Stat{}.Build() where Stat{} belongs), a pointer given as a component (&Stat{}), a prop in a field of type tela.Component and a nil dependency list (UseEffect(f, nil): use tela.Once or UseMount) panic with the fix.
  • Likely mistakes are warnings, printed once per call site (test log lines in telatest): a list without keys, props that a tela.CP component cannot compare, a Value.Get that misses queued changes (use Latest), a context without a Provider whose default would panic, a Label that labels nothing.

telatest.NoDev() tests what a release build does.

A release build is a plain go build. Build with -trimpath: Tela then treats the binary as a release build and turns dev mode off (see above).

Terminal window
CGO_ENABLED=0 go build -trimpath -o counter .
CGO_ENABLED=0 GOOS=windows go build -trimpath -ldflags="-H=windowsgui" -o counter.exe .

On Windows, add -ldflags="-H=windowsgui". Go builds console programs by default, and Windows opens a console window next to the app when a console program is started from Explorer, a shortcut or the Start menu. -H=windowsgui makes the executable a GUI program: only the app’s window opens. Such a program has no standard output or error, so whatever it prints (dev warnings, a panic’s message) goes nowhere; build without the flag, or run the app from a terminal, while you debug. go run and tela dev need nothing: they use the terminal you start them from.

Tela does not hide the console at run time: the window would still flash on screen, which looks like malware. A future tela build sets the flag for Windows targets and writes panics to a log file (ADR-127).

  • Styling to make it look good.
  • Hooks for state, effects and context.
  • Events and Forms and Text Input for interaction.
  • The examples/ directory has complete screens (dashboard, signup form, kanban board, music player), each with its tests.