Skip to content

Getting Started

Andres Felipe Martinez Salazar edited this page Nov 2, 2026 · 5 revisions

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.

Requirements

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

Create the module

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

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

Write the app

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 items-center justify-center h-full bg-stone-200"),
		Div(Class("flex flex-col items-center gap-6 w-80 p-10 rounded-2xl",
			"bg-stone-50 border border-stone-300 shadow-xl text-stone-800"),
			Span(Class("text-sm font-medium tracking-widest text-stone-500"), Text("COUNT")),
			H1(Class("text-8xl font-bold tracking-tight tabular-nums"), Textf("%d", count)),
			Button(Class("w-full py-3 rounded-xl bg-stone-800 text-stone-100 font-medium",
				"hover:bg-stone-700 active:scale-95 transition"),
				OnClick(func() { setCount.Update(func(n int) int { return n + 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: 400,
		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 takes a func(), or a func(tela.MouseEvent) when the handler reads the event.
  • 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.

Components with props

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.

Run it

CGO_ENABLED=0 go run .

The counter rendered by Tela

Render it without a window

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:

CGO_ENABLED=0 go install github.com/kuronosu/tela/cmd/tela@latest

tela snapshot --size 480x400 -o counter.png .
tela inspect --size 480x400 .

(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.

- text: COUNT
- heading "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.

Test it

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, 400))

	app.Click(telatest.ByRole("button", "Increment"))

	app.ExpectText("COUNT 1 Increment")
}

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

Dev mode

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.

Ship it

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).

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).

Next steps

  • 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 (a dashboard with a kanban board, login and sign-up forms, music player), each with its tests.

Clone this wiki locally