Skip to content

API reference: package h

import "github.com/kuronosu/tela/h"

This page is generated from the doc comments of the package; the same text is on pkg.go.dev. For guides and examples, start at the wiki home.

Package h holds the elements, props and helpers of a Tela UI. It is designed to be dot-imported, so that markup reads like JSX:

import (
"github.com/kuronosu/tela"
. "github.com/kuronosu/tela/h"
)
func Greeting(name string) tela.Node {
return Div(Class("flex items-center gap-3 rounded-xl bg-white p-4 shadow-md"),
H2(Class("text-lg font-semibold text-slate-900"), Textf("Hello, %s", name)),
If(name == "", P(Class("text-slate-500"), Text("Who are you?"))),
)
}

To keep collisions with the importing package unlikely, h exports only element constructors (Div, Span, Text, H1 to H6, P, Button, Img, Strong, Em, Code, A, and the form elements Input, Textarea, Form and Label), props (Class, Style, Key, Disabled, Scheme, Src, Alt, Href, ID, the focus props TabIndex, AutoFocus and Ref, the semantics props Role and Aria…, the field props Value, DefaultValue, Bind, BindValue, Type, Rows, Placeholder, ReadOnly, MaxLength, AutoComplete, SpellCheck, Name and For, the custom title bar props WindowDrag, WindowNoDrag and WindowControl, and the event handlers OnClick, OnDoubleClick, OnContextMenu, OnMouse…, OnPointer…, OnDrag…, OnLongPress, OnKey…, OnFocus, OnBlur, OnWheel, OnScroll, OnChange, OnBeforeInput, OnComposition…, OnSelect and OnSubmit), the helpers If, IfElse (for nodes and props alike), Map, MapIndex, MapKeyed, Range, Fragment and TextFn (text that a Value changes without rebuilding the component), Portal (content shown above the window’s content, for modals, menus and tooltips), VirtualList with its Overscan prop, and Presence with its Exit prop (enter and exit animations).

Every event handler takes a func(), a func of its event, or the result of tela.StopPropagation(f), which stops the event’s propagation and then calls f.

Everything else (App, Run, hooks, events, colors, typed style props) lives in package tela.

An element takes its props and children in one list, in any order:

Div(Class("p-4"), Key(id), Text("hi"), Span(Text("there")))

Props apply in order, so a later Class wins over an earlier one and Style wins over every class. Children are nodes or components (a tela.Component value, such as a struct with a Build method, is a component with its own state); wrap text in Text or Textf. The parameters are tela.Child, so passing a bare string is a compile error, which names the internal type behind the alias: “string does not implement core.Child (missing method Build)” means Div(“hi”) should be Div(Text(“hi”)).

Elements have the defaults of Tailwind’s Preflight: no margins, padding or borders, headings in the surrounding font size, block images. A Button has no background of its own, and neither has an Input or a Textarea: style fields with classes.

Text fields are controlled, like React’s:

func Signup() tela.Node {
email, setEmail := tela.UseState("")
return Form(Class("flex flex-col gap-2"), OnSubmit(func() { register(email) }),
Label(For("email"), Class("text-sm font-medium"), Text("Email")),
Input(ID("email"), Type("email"), Bind(email, setEmail), Placeholder("you@example.com"),
Class("rounded-md border border-slate-300 px-3 py-2 invalid:border-red-500")),
Button(Class("rounded-md bg-indigo-600 px-4 py-2 text-white"), Text("Sign up")))
}
func A(children ...tela.Child) tela.Node

A returns a link: inline text with a destination given by Href. Links are unstyled (no color or underline), like Tailwind’s Preflight; style them with classes.

func Alt(text string) tela.Prop

Alt sets the alternative text of an Img: its accessible name. Use Alt(“”) for a decorative image, which the semantics tree leaves out. It panics on other elements.

Img(Src(avatar), Alt("Ada Lovelace"), Class("size-10 rounded-full"))
func AnimateLayout() tela.Prop

AnimateLayout makes the element animate every change of its laid-out box (Motion’s layout prop): when a reorder, an expand or collapse, a sibling inserted or removed, or a breakpoint moves or resizes it, it is laid out at its new place at once but drawn moving there from where it was, through a transform (FLIP). Layout is not run again during the animation, so its frames only composite.

Map(items, func(it Item) tela.Node {
return Div(Key(it.ID), AnimateLayout(), Class("rounded-lg bg-white p-3 shadow-sm"),
Text(it.Title))
})

The motion is a tween with the element’s duration-*, ease-* (and delay-*) classes when it has one of the first two, like a transition; otherwise a spring (stiffness 500, damping 25: Motion’s spring for x and y). A change during the animation starts from where the element is drawn, keeping its speed.

A size change is drawn as a scale toward the new size, as in Motion: the element’s rounded corners and box shadows are corrected so they keep their look, but its text and children stretch for the duration of the animation, unless they have AnimateLayout too: a child with it keeps its own size and its place in the parent (put it on a card’s content to keep the text crisp while the card grows). A clip-path and background images scale with the element, as under a CSS transform.

Scrolling, transforms (hover:scale-105) and the display scale do not move an element’s layout, so they never start an animation. While it animates, the element is hit by the pointer, and ElementRef.Bounds and tela.UseBounds report its box, where it is drawn (like getBoundingClientRect under a CSS transform); scrolling it into view uses its final place. With reduced motion, and without a frame clock (tela snapshot), it is drawn at its new place at once. telatest waits for layout animations like for other animations.

Example

t := &exampleT{}
defer t.close()
app := telatest.Mount(t, tela.C(fruitList))
before := app.Bounds(telatest.ByText("Apples")).Y
app.Click(telatest.ByRole("button", "Reverse"))
// The row is laid out at the bottom at once and drawn sliding there;
// telatest waits for the animation before it measures.
after := app.Bounds(telatest.ByText("Apples")).Y
fmt.Println(before, after)
// Output: 48 144
func AriaActiveDescendant(ref tela.ElementRef) tela.Prop

AriaActiveDescendant names the active descendant of a composite widget that keeps the focus itself, attached to ref with Ref: the option a combobox’s arrow keys moved to while the focus stays in its input.

Input(Role("combobox"), AriaActiveDescendant(active), AriaControls(list))
func AriaAutoComplete(mode string) tela.Prop

AriaAutoComplete describes the completion a combobox or textbox offers: “inline” (the rest of the word is typed ahead), “list” (a list of suggestions), “both” or “none”. An unknown value panics in dev mode.

Input(Role("combobox"), AriaAutoComplete("list"), AriaExpanded(open))
func AriaBusy(busy bool) tela.Prop

AriaBusy marks an element, and what it contains, as being updated (aria-busy): a list that loads. The aria-busy: variants style it.

Div(AriaBusy(loading), Class("aria-busy:opacity-50 aria-busy:animate-pulse"), rows)
func AriaChecked(checked bool) tela.Prop

AriaChecked sets the checked state of a checkbox, radio or switch.

Div(Role("checkbox"), AriaChecked(done), TabIndex(0), Text("Done"))
func AriaControls(ref tela.ElementRef) tela.Prop

AriaControls names the element whose content or visibility this one controls, attached to ref with Ref: the menu of a menu button, the panel of a tab, the listbox of a combobox.

menu := tela.UseElementRef()
Button(AriaControls(menu), AriaExpanded(open), Text("File")),
If(open, Div(Ref(menu), Role("menu"), items))
func AriaCurrent(current string) tela.Prop

AriaCurrent marks the current item of a set (aria-current): “page” for the link of the page being shown, “step”, “location”, “date” or “time” for the current item of a process, a trail, a calendar or a timeline, and “true” for any other. “false” and “” mean none (the default). Unknown tokens panic in dev mode with the closest valid one. The aria-[current=page]: variant (and step, location, date, time, true, with their group- and peer- forms) styles it; Tailwind has no bare aria-current:. The semantics node carries it for assistive technologies; the aria snapshot does not print it, as Playwright’s does not.

A(Href("/docs"), AriaCurrent(current), // "page" on the active link, "" elsewhere
Class("text-muted-foreground aria-[current=page]:text-foreground aria-[current=page]:font-medium"),
Text("Docs"))
func AriaDescribedBy(ref tela.ElementRef) tela.Prop

AriaDescribedBy describes an element with the text of another one, attached to a ref with Ref (HTML’s aria-describedby): a tooltip describes its trigger. AriaDescription wins over it.

Div(Role("tooltip"), Ref(tip), Text("Saves the file")),
Button(AriaDescribedBy(tip), Text("Save"))
func AriaDescription(description string) tela.Prop

AriaDescription sets the accessible description of an element: a longer hint read after its name.

Button(AriaDescription("Removes the file for everyone"), Text("Delete"))
func AriaDisabled(disabled bool) tela.Prop

AriaDisabled marks an element and its descendants as disabled for assistive technologies without making them inert (Disabled does both).

Button(AriaDisabled(true), OnClick(explainWhy), Text("Publish"))
func AriaExpanded(expanded bool) tela.Prop

AriaExpanded sets whether what a button or link controls (a menu, a disclosure) is expanded.

Button(AriaExpanded(open), OnClick(toggle), Text("Options"))
func AriaHasPopup(kind string) tela.Prop

AriaHasPopup sets the kind of popup an element opens: “menu”, “listbox”, “tree”, “grid” or “dialog” (“true” means “menu”, “false” none). An unknown value panics in dev mode with the closest valid one.

Button(AriaHasPopup("menu"), AriaExpanded(open), OnClick(toggle), Text("Options"))
func AriaHidden(hidden bool) tela.Prop

AriaHidden hides an element and its subtree from the semantics tree when true (decorations, duplicated content), without hiding it on screen.

Span(AriaHidden(true), Text("★"))
func AriaInvalid(invalid bool) tela.Prop

AriaInvalid marks a field whose value failed the app’s validation (aria-invalid): a text field, checkbox, combobox or listbox. Assistive technologies announce it, the aria snapshot prints it as [invalid] and the aria-invalid: variants style it. An Input whose own value is invalid (an email Type with a malformed address) is already invalid for assistive technologies; AriaInvalid, true or false, overrides that.

Input(Bind(email, setEmail), AriaInvalid(err != ""),
Class("border aria-invalid:border-destructive aria-invalid:ring-destructive/20"))
func AriaLabel(label string) tela.Prop

AriaLabel sets the accessible name of an element, for elements whose content does not name them (an icon button).

Button(AriaLabel("Close"), OnClick(close), closeIcon)
func AriaLabelledBy(refs ...tela.ElementRef) tela.Prop

AriaLabelledBy names an element with the text of other elements, attached to refs with Ref. With several references the name is the text of each one that is mounted, in the order given, joined by a space (aria-labelledby=“a b”), as a select is named by its value and its label.

title := tela.UseElementRef()
Div(Role("dialog"), AriaLabelledBy(title),
H2(Ref(title), Text("Delete file?")), ...)
Button(AriaLabelledBy(value, label), ...)

Calling it with no reference, or with the zero ElementRef, panics.

func AriaLevel(level int) tela.Prop

AriaLevel sets the level of an element with Role(“heading”) (H1 to H6 have theirs), or of a list item, row or tree item.

Div(Role("heading"), AriaLevel(2), Text("Billing"))
func AriaModal(modal bool) tela.Prop

AriaModal marks a dialog as modal: the rest of the window is not available while it is open. It only describes the dialog; package aria’s UseDialog also makes the outside inert.

Div(Role("dialog"), AriaModal(true), AriaLabelledBy(title), ...)
func AriaMultiSelectable(multi bool) tela.Prop

AriaMultiSelectable marks a listbox, grid, tablist or tree in which several items can be selected.

Div(Role("listbox"), AriaMultiSelectable(true), options)
func AriaOrientation(orientation string) tela.Prop

AriaOrientation sets the orientation of a listbox, menu, tablist, toolbar, slider or separator: “horizontal”, “vertical” or “undefined”. An unknown value panics in dev mode.

Div(Role("tablist"), AriaOrientation("vertical"), tabs)
func AriaPressed(pressed bool) tela.Prop

AriaPressed makes a button a toggle button and sets whether it is pressed.

Button(AriaPressed(bold), OnClick(toggleBold), Text("B"))
func AriaReadOnly(readOnly bool) tela.Prop

AriaReadOnly marks a widget the user can focus but not edit (aria-readonly): a text field, checkbox, slider or listbox built from divs. An Input or Textarea with ReadOnly is already read-only; the aria-readonly: variants style the elements with AriaReadOnly(true).

Div(Role("textbox"), AriaReadOnly(locked), Class("aria-readonly:bg-slate-100"))
func AriaRequired(required bool) tela.Prop

AriaRequired marks a field the user must fill in (aria-required): a text field, combobox, listbox or radio group. It changes nothing on screen; the aria-required: variants style it.

Input(ID("email"), AriaRequired(true), Class("border aria-required:border-indigo-400"))
func AriaSelected(selected bool) tela.Prop

AriaSelected sets whether a tab, option, row or cell is selected.

Button(Role("tab"), AriaSelected(active == "general"), Text("General"))
func AriaSort(sort string) tela.Prop

AriaSort tells how a table is sorted by a column header (aria-sort): “ascending”, “descending”, “other” or “none” (the default). Unknown values panic in dev mode with the closest valid one. The aria-[sort=ascending]: and aria-[sort=descending]: variants style the header, and their group- forms what it contains, such as its arrow.

Div(Role("columnheader"), AriaSort(order), OnClick(toggleOrder), Class("group flex gap-1"),
Text("Name"), Icon(arrowUp, Class("size-4 group-aria-[sort=descending]:rotate-180")))
func AriaValueMax(value float64) tela.Prop

AriaValueMax sets the maximum value of a range widget.

func AriaValueMin(value float64) tela.Prop

AriaValueMin sets the minimum value of a range widget.

func AriaValueNow(value float64) tela.Prop

AriaValueNow sets the current value of a range widget (Role(“slider”), “progressbar”, “spinbutton”, “meter”).

Div(Role("progressbar"), AriaValueNow(40), AriaValueMin(0), AriaValueMax(100), bar)
func AriaValueText(text string) tela.Prop

AriaValueText sets the value of a range widget as people read it.

Div(Role("slider"), AriaValueNow(3), AriaValueText("Medium"), track)
func AutoComplete(s string) tela.Prop

AutoComplete is HTML’s autocomplete hint (“email”, “current-password”…). It is accepted for familiarity; Tela has no autofill.

func AutoFocus(on bool) tela.Prop

AutoFocus focuses the element when it first appears, in the frame that shows it (React’s autoFocus), unless another element took the focus since it was mounted. When several AutoFocus elements appear at once, the first in tree order wins.

Button(AutoFocus(true), OnClick(confirm), Text("OK"))
func Bind[S tela.Setter[string] | ~func(string)](text string, set S) tela.Prop

Bind makes a text field controlled by a state: Value(text) plus an OnChange that sends every new value to set, a tela.Setter or any func(string) (of a named func type too), such as one that dispatches an action to a reducer. A nil function panics.

name, setName := tela.UseState("")
Input(Bind(name, setName))
form, d := tela.UseReducer(reduce, Form{})
Input(Bind(form.Email, func(s string) { d.Dispatch(setEmail(s)) }))

Example (func)

type action struct{ text string }
form := func() tela.Node {
text, d := tela.UseReducer(func(_ string, a action) string { return a.text }, "")
// A func(string) binds as well as a Setter: here, a Dispatcher.
return Div(
Input(AriaLabel("Name"), Bind(text, func(s string) { d.Dispatch(action{s}) })),
P(Textf("Hello, %s", text)))
}
t := &exampleT{}
defer t.close()
app := telatest.Mount(t, tela.C(form), telatest.Size(300, 100))
app.Fill(telatest.ByRole("textbox", "Name"), "Ada")
fmt.Println(app.AriaSnapshot())
// Output:
// - textbox "Name": Ada
// - paragraph: Hello, Ada
func BindValue(v *tela.Value[string]) tela.Prop

BindValue binds a text field to v, from tela.UseValue: the field shows v’s value and writes every edit to it without rebuilding the component (only what reads v rebuilds or repaints), and follows v.Set from anywhere.

query := tela.UseValue("")
Input(Type("search"), BindValue(query))
func Button(children ...tela.Child) tela.Node

Button returns a button. It has no background, border or padding of its own (Tailwind’s Preflight): style it with classes. Its width fits its content, it sits on the line of the text around it (inline-block, on the baseline; Class(“align-middle”) moves it), and it shows the pointer cursor. Clicks are handled with OnClick.

func Class(parts ...string) tela.Prop

Class adds Tailwind v4 classes (“flex items-center gap-4 hover:bg-white/10”). It takes several strings and joins them with one space, skipping the empty ones, so a class that depends on state needs no concatenation:

tone := "text-slate-500"
if active {
tone = "text-indigo-600 font-semibold"
}
Div(Class("flex items-center gap-2 rounded-lg px-3 py-1", tone))

It may repeat on an element; the strings concatenate in order and a later class wins over an earlier one that sets the same property. An unknown class is reported in dev mode (on in development builds, see tela.App.Dev) with the closest valid one. For a whole prop that depends on a condition, If(selected, Class(“ring-2”)) is the form.

Example

// A class is a Tailwind v4 class: bg-red-500 paints the box red.
swatch := Div(Class("size-16 bg-red-500"))
img, _ := telatest.Render(swatch, telatest.Size(64, 64))
fmt.Println(img.RGBAAt(32, 32))
// Output:
// {251 44 54 255}

Example (parts)

// Parts are joined with one space and empty ones are skipped: no
// concatenation, no trailing-space convention.
tab := func(active bool) tela.Node {
tone := "bg-white text-slate-500"
if active {
tone = "bg-indigo-600 text-white"
}
return Div(Class("size-16 rounded-lg", "", tone))
}
img, _ := telatest.Render(tab(true), telatest.Size(64, 64))
fmt.Println(img.RGBAAt(32, 32))
// Output: {79 57 246 255}
func Code(children ...tela.Child) tela.Node

Code returns inline text in the monospace font.

func DefaultValue(s string) tela.Prop

DefaultValue gives an uncontrolled text field its initial value; later values are ignored (React’s defaultValue). Read the value with tela.ElementRef’s Value.

func Dir(dir string) tela.Prop

Dir sets the direction of an element and its subtree, like HTML’s dir attribute: “rtl” lays it out right to left, “ltr” left to right, and “auto” takes the direction of the first strong character of its text (of its value for an Input or Textarea). Direction is inherited; the root is left to right. The value is case-insensitive and “” inherits.

Under “rtl” layout mirrors like a browser’s: flex rows start at the right, grid columns count from the right, text aligns right by default (text-start, text-end), paragraphs have a right-to-left base direction, absolutely positioned boxes take their static position from the right, and the logical utilities (ms-*, me-*, ps-*, pe-*, start-*, end-*, inset-s-*, border-s-*, rounded-s-*, rounded-ss-*, space-x-*…) use the right as the start side. Physical classes (ml-*, left-*, text-left) stay physical. The rtl: and ltr: variants test the direction:

Div(Dir("rtl"), Class("flex gap-2 ps-4 rtl:font-bold"), ...)

With package i18n, the direction of the current locale:

loc := i18n.UseLocale()
Div(Dir(i18n.Dir(loc).String()), ...)

Any other value panics in dev mode and is ignored otherwise.

func Disabled(disabled bool) tela.Prop

Disabled disables an element, like a disabled form control: no pointer handler runs on it or on its descendants, and their events do not reach it (its ancestors still receive them); it does not become active, and neither it nor its descendants can take the focus (a focused one loses it). Its disabled: and enabled: variants apply, as do the group-disabled: and peer-disabled: variants of others.

Button(Disabled(saving), Class("bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"),
OnClick(save), Text("Save"))

Example

save := Button(Disabled(true), Class("disabled:opacity-50"), Text("Save"))
yaml, _ := telatest.AriaSnapshot(save)
fmt.Println(yaml)
// Output:
// - button "Save" [disabled]
func Div(children ...tela.Child) tela.Node

Div returns a block container, the general-purpose box. Its children stack vertically unless its classes say otherwise (flex).

Example

card := Div(Class("flex flex-col gap-2 rounded-xl bg-white p-6 shadow-md"),
H2(Class("text-lg font-semibold"), Text("Inbox")),
P(Class("text-slate-500"), Textf("%d unread messages", 3)),
Button(Class("rounded-md bg-indigo-600 px-4 py-2 text-white"), Text("Open")),
)
yaml, _ := telatest.AriaSnapshot(card)
fmt.Println(yaml)
// Output:
// - heading "Inbox" [level=2]
// - paragraph: 3 unread messages
// - button "Open"
func Em(children ...tela.Child) tela.Node

Em returns italic inline text.

func Exit(classes string) tela.Prop

Exit sets the classes an element takes when the child of a Presence it is in is removed: they apply on top of its classes, and its transitions (transition, duration-*, ease-*, delay-*) run to them before the child unmounts. The usual exit is the reverse of the entrance:

Div(Class("transition duration-150 starting:opacity-0 starting:translate-y-2"),
Exit("opacity-0 translate-y-2"), ...)

Any element of the removed child may have Exit classes (a dialog’s backdrop and panel can fade differently); the child unmounts when all their transitions have ended. Outside a Presence, Exit does nothing. It may repeat; the strings concatenate. Unknown classes are reported in dev mode like those of Class.

Example

t := &exampleT{}
defer t.close()
var style string
toast := func() tela.Node {
shown, setShown := tela.UseState(true)
return Div(
Button(OnClick(func(tela.MouseEvent) { setShown.Set(false) }), Text("Dismiss")),
// The backdrop and the card each have their own exit; the
// child unmounts when both transitions have ended.
Presence(If(shown, Div(Key("toast"),
Div(Class("pointer-events-none fixed inset-0 bg-black/20 transition-opacity duration-300"), Exit("opacity-0")),
Div(Role("status"), Class("rounded-lg bg-slate-900 p-3 text-white transition duration-150"),
Exit("opacity-0 translate-y-2"), Text("Saved"))))))
}
app := telatest.Mount(t, tela.C(toast))
start := app.Now()
app.Click(telatest.ByRole("button", "Dismiss")) // waits for the exit
if app.Now()-start >= 300*time.Millisecond {
style = "waited for the slower exit"
}
fmt.Println(style, app.Count(telatest.ByRole("status", "")))
// Output: waited for the slower exit 0
func For(id string) tela.Prop

For makes a Label label the element whose ID is id.

func Form(children ...tela.Child) tela.Node

Form returns a form: a block that groups fields and buttons. Enter in one of its single-line fields, or a click on a Button inside it, submits it (OnSubmit); a Button with Type(“reset”) restores the DefaultValue of its uncontrolled fields. Buttons inside a form are submit buttons unless Type says “button”.

Form(OnSubmit(func() { save() }),
Input(Bind(name, setName)),
Button(Type("submit"), Text("Save")))
func Fragment(children ...tela.Child) tela.Node

Fragment groups children without a box of their own: they become children of the fragment’s parent. It takes only Key as a prop.

It is how a component returns several siblings, how a wrapper puts the children it receives next to its own (Go does not allow Div(Class(“…”), H2(…), children…), but Fragment(children…) is one child), and how several children go in one field of type tela.Component. The children must be nodes or components (or Key):

// Card has no hooks, so it is a plain function that wraps its children.
func Card(title string, children ...tela.Child) tela.Node {
return Div(Class("rounded-xl border p-4"),
H2(Class("font-semibold"), Text(title)),
Fragment(children...))
}
Panel{Title: "Inbox", Children: Fragment(Text("a"), Span(Text("b")))}

Fragment(c) is also a node of one component, for a function that must return a tela.Node: return Fragment(Panel{…}).

func H1(children ...tela.Child) tela.Node

H1 returns a level 1 heading. Headings are unstyled: size and weight come from classes (text-5xl font-bold).

func H2(children ...tela.Child) tela.Node

H2 returns a level 2 heading. See H1.

func H3(children ...tela.Child) tela.Node

H3 returns a level 3 heading. See H1.

func H4(children ...tela.Child) tela.Node

H4 returns a level 4 heading. See H1.

func H5(children ...tela.Child) tela.Node

H5 returns a level 5 heading. See H1.

func H6(children ...tela.Child) tela.Node

H6 returns a level 6 heading. See H1.

func Href(url string) tela.Prop

Href sets the destination of an A. Tela does not open links by itself yet: handle OnClick.

func ID(id string) tela.Prop

ID gives an element an identifier, unique in the window, that a Label’s For refers to (HTML’s id). tela.UseID makes one for a reusable field.

func Icon(icon tela.Icon, props ...tela.Child) tela.Node

Icon returns an SVG icon (tela.Icon), drawn as a vector at any size and scale. It is 1em by 1em (the element’s font size, like an icon font) unless a size-*, w-* or h-* class or a Style sizes it, and a block, like an svg under Tailwind’s Preflight (Class(“inline”) puts it on a line of text; in a flex row it is an item). Its currentColor is the text color, so text-* classes, theme tokens, TextColorFn and transitions recolor it without parsing it again; fill-*, stroke-* and stroke-[1.5] classes, on it or inherited from an ancestor (a Button’s, as shadcn’s [&_svg] classes do), override the document’s own fill, stroke and stroke-width.

Button(Class("inline-flex items-center gap-2 text-sm"),
Icon(lucide.Check, Class("size-4 stroke-[1.5]")), Text("Save"))

An icon is decorative: hidden from accessibility unless it has an AriaLabel or AriaLabelledBy, which make it an image with that name. An SVG that cannot be parsed renders nothing and is reported once in the log.

Example

t := &exampleT{}
defer t.close()
ui := Div(Class("flex gap-2 text-2xl"),
// Decorative: hidden from accessibility; the button is named by
// its text. Its color is the text color.
Button(Class("inline-flex items-center gap-2 text-red-600"),
Icon(check, Class("size-4 stroke-[1.5]")), Text("Save")),
// Named: an image. Without a size it is 1em: 24 px here.
Button(Icon(check, AriaLabel("Done"))),
)
app := telatest.Mount(t, ui)
fmt.Println(app.AriaSnapshot())
r := app.Bounds(telatest.ByRole("img", "Done"))
fmt.Println(r.Width, r.Height)
// Output:
// - button "Save"
// - button "Done":
// - img "Done"
// 24 24
func If(cond bool, c tela.Child) tela.Child

If returns c when cond holds and an empty child otherwise: one that renders nothing and applies nothing. It takes a node, a struct component or a prop, so it works for children and for props: the empty child keeps the positions of the siblings after it (toggling cond does not remount them).

Div(
If(loading, Spinner{}),
If(selected, Class("ring-2 ring-indigo-500")),
If(err != nil, P(Class("text-red-600"), Text(err.Error()))),
)

The result is a tela.Child, so it goes wherever one does: the children of an element, Fragment, Presence, Portal and a Provider, and a field of type tela.Child or tela.Component. It is not a tela.Node: a function component that returns it wraps it, return Fragment(If(…)).

Every argument of a call is evaluated: If(user != nil, Text(user.Name)) dereferences a nil user. Use an ordinary if for that case.

Example

screen := func(loading bool, err error) tela.Node {
return Div(
If(loading, P(Text("Loading..."))),
If(err != nil, P(Class("text-red-600"), Text("Something failed"))),
P(Text("Done")),
)
}
yaml, _ := telatest.AriaSnapshot(screen(true, nil))
fmt.Println(yaml)
// Output:
// - paragraph: Loading...
// - paragraph: Done

Example (structComponent)

screen := func(loading bool) tela.Node {
return Div(
If(loading, Spinner{Label: "Loading"}),
P(Text("Inbox")),
)
}
for _, loading := range []bool{true, false} {
yaml, _ := telatest.AriaSnapshot(screen(loading))
fmt.Println(yaml)
}
// Output:
// - status "Loading"
// - paragraph: Inbox
// - paragraph: Inbox
func IfElse(cond bool, a, b tela.Child) tela.Child

IfElse returns a when cond holds and b otherwise: two nodes, two struct components or two props. A branch that is itself empty (the nil child, the zero Node, the result of an If that is off) renders nothing.

Div(IfElse(open, Class("h-64"), Class("h-0")),
IfElse(loading, Spinner{}, Text(label)))

Both arguments are evaluated, like those of If.

Example

toggle := func(on bool) tela.Node {
return Button(
IfElse(on, Class("bg-indigo-600 text-white"), Class("bg-slate-200 text-slate-900")),
AriaPressed(on),
IfElse(on, Text("On"), Text("Off")),
)
}
yaml, _ := telatest.AriaSnapshot(toggle(true))
fmt.Println(yaml)
// Output:
// - button "On" [pressed]
func Img(children ...tela.Child) tela.Node

Img returns an image. Give it a source with Src (a decoded image), SrcPath (a file of the file systems registered with tela.Assets), SrcBytes or SrcFS; PNG, JPEG, GIF, WebP and SVG are recognized by their content, and an SVG stays vector. Without Class or Style sizes it is drawn at the size of the image, shrunk to fit its container; with one size (h-8) the other follows the image’s aspect ratio, as for an HTML img. object-contain, object-cover and the other object-* classes fit it in its box, and Tint paints it in one color.

func Inert(inert bool) tela.Prop

Inert makes an element and its subtree inert when true, like HTML’s inert attribute: they are not hit by the pointer (a press goes to what is under them), cannot take the focus (a focused element in them loses it) and leave the Tab order, and assistive technologies, telatest’s locators and the aria snapshot do not see them. They are still painted: dim them with classes if they should look disabled. Content rendered from inside the element through a Portal is inert too.

cls := "space-y-4"
if saving {
cls += " opacity-50"
}
Div(Inert(saving), Class(cls), fields)
func Input(children ...tela.Child) tela.Node

Input returns a single-line text field. Like a browser’s, it edits its value with the keyboard (typing, arrows and Ctrl+arrows, Home/End, Backspace/Delete, Ctrl+A, Ctrl+Z/Ctrl+Y, the clipboard), the mouse (click, drag, double and triple click) and input methods (IME), and submits its Form on Enter. Give it a value with Value and OnChange (or Bind: controlled), DefaultValue (uncontrolled) or BindValue, a Type, a Placeholder and a Label.

It has Tailwind’s Preflight defaults: no border, padding or background (style it with classes), the inherited font, and a width of about 20 characters (Chromium’s size=20) unless a class sets one; its text is centered vertically in its height. field-sizing-content sizes it to its text. It takes no children.

Input(Type("email"), Placeholder("you@example.com"), Bind(email, setEmail),
Class("w-full rounded-md border border-slate-300 px-3 py-2 placeholder:text-slate-400 "+
"focus:outline-2 focus:outline-indigo-500"))

Example

// A controlled field: the value comes from state, and Bind writes
// every edit back to it.
signup := func() tela.Node {
email, setEmail := tela.UseState("ana@example.com")
return Form(Class("flex flex-col gap-2"),
Label(For("email"), Text("Email")),
Input(ID("email"), Type("email"), Bind(email, setEmail),
Class("rounded-md border border-slate-300 px-3 py-2")),
Button(Class("rounded-md bg-indigo-600 px-4 py-2 text-white"), Text("Sign up")),
)
}
yaml, _ := telatest.AriaSnapshot(tela.C(signup))
fmt.Println(yaml)
// Output:
// - text: Email
// - textbox "Email": ana@example.com
// - button "Sign up"
func ItemHeight(px float32) tela.Prop

ItemHeight gives every item of a VirtualList the same height in logical pixels, 1 or more. The items are not measured, so the list does no per-item measuring work, the scroll positions are exact from the first frame and the scrollbar does not change while scrolling; the estimateSize argument is not needed (pass nil). An item taller than the height spills over the next one (it is not clipped).

VirtualList(len(rows), nil, func(i int) tela.Component { return Row{Row: rows[i]} },
ItemHeight(40), Class("h-96"))

Example

// Rows of one height need no estimate and are never measured.
rows := VirtualList(1_000_000, nil,
func(i int) tela.Node { return Div(Class("truncate"), Textf("Row %d", i)) },
ItemHeight(40), Class("h-[120px]"), Overscan(0))
yaml, _ := telatest.AriaSnapshot(rows, telatest.Size(200, 120))
fmt.Println(yaml)
// Output:
// - list:
// - listitem: Row 0
// - listitem: Row 1
// - listitem: Row 2
func ItemKey[K comparable](key func(i int) K) tela.Prop

ItemKey keys the items of a VirtualList by key(i), the identity of item i, instead of by its index, so that the state of an item component follows the item when others are inserted or removed before it (Key’s rule for lists, and TanStack Virtual’s getItemKey). K is any comparable type, such as an ID; a nil key or one that cannot be compared panics when the list builds. Give the key function with the data the list shows: it runs for the items that are built, with the indices of the list’s current items.

The heights measured so far stay with the indices, not with the items: after an insertion the items that moved are measured again when they are laid out.

VirtualList(len(rows), estimate,
func(i int) tela.Component { return Row{Row: rows[i]} },
ItemKey(func(i int) int { return rows[i].ID }))

Example

t := &exampleT{}
defer t.close()
var set tela.Setter[[]todoItem]
list := func() tela.Node {
todos, s := tela.UseState([]todoItem{{1, "Write"}, {2, "Test"}})
set = s
return VirtualList(len(todos), func(int) float32 { return 30 },
func(i int) tela.Node { return Fragment(todos[i]) },
// The state of an item follows its ID, not its position.
ItemKey(func(i int) int { return todos[i].ID }), Class("h-24"))
}
app := telatest.Mount(t, tela.C(list), telatest.Size(200, 100))
app.Click(telatest.ByRole("checkbox", "Test"))
set.Set([]todoItem{{3, "Plan"}, {1, "Write"}, {2, "Test"}})
fmt.Println(app.AriaSnapshot())
// Output:
// - list:
// - listitem:
// - checkbox "Plan"
// - listitem:
// - checkbox "Write"
// - listitem:
// - checkbox "Test" [checked]
func Key[K comparable](k K) tela.Prop

Key gives an element an identity among its siblings: when a list changes, the element with the same key keeps its state wherever it moves. k is a comparable value, such as an item ID: a slice, a map or a func does not compile, and a nil interface, or one holding a value that cannot be compared, panics. Elements built from data (Map) should have keys. A component or a fragment takes its key from tela.WithKey.

func Label(children ...tela.Child) tela.Node

Label returns a label for a field: inline text that gives the field its accessible name and focuses it when clicked. It labels the element whose ID is its For, or else the Input, Textarea or Button inside it.

Label(For("name"), Text("Name")), Input(ID("name"))
Label(Text("Name "), Input())

A Label that labels nothing (For naming no mounted Input, Textarea or Button, and no such element inside) names nothing and focuses nothing; dev mode warns about it once. For a caption that labels no field, use Span or P.

func LayoutID(id string) tela.Prop

LayoutID gives the element a shared layout identity (Motion’s layoutId): when an element with a LayoutID mounts in the same commit in which another element with the same ID unmounts, or while one is exiting under a Presence, the new element starts from the old one’s box and animates to its own, as AnimateLayout does (a shared element transition). It is how a tab underline slides between tabs and a thumbnail grows into a detail view:

Map(tabs, func(t Tab) tela.Node {
return Button(Key(t.ID), Class("relative px-3 py-2"), OnClick(...), Text(t.Label),
If(t.ID == active, Div(LayoutID("underline"),
Class("absolute inset-x-0 -bottom-px h-0.5 bg-indigo-500"))))
})

An element with a LayoutID also animates its own layout changes, like AnimateLayout. IDs are global to the window: use distinct ones for unrelated transitions. The element that unmounts or exits is not moved: give an exiting one Exit classes (opacity-0) to fade it out while the new one grows from it.

Example

t := &exampleT{}
defer t.close()
app := telatest.Mount(t, tela.C(tabBar))
underline := telatest.ByClass("bg-indigo-500")
x0 := app.Bounds(underline).X
app.Click(telatest.ByRole("button", "Settings"))
x1 := app.Bounds(underline).X
fmt.Println(x0 < x1)
// Output: true
func Map[T any](items []T, f func(T) tela.Node) tela.Node

Map returns the nodes of f applied to every item, as one list. Give each node a Key (the item’s ID, not its index) so that reordering, inserting and removing keeps their state; dev mode (see tela.App.Dev) warns about a list of two or more nodes without keys, and about duplicate keys, once per call site of Map.

Map(todos, func(t Todo) tela.Node {
return Div(Key(t.ID), Text(t.Title))
})

Example

type Todo struct {
ID int
Text string
}
todos := []Todo{{1, "Write docs"}, {2, "Ship it"}}
list := Div(Role("list"),
Map(todos, func(t Todo) tela.Node {
return Div(Key(t.ID), Role("listitem"), Text(t.Text))
}),
)
yaml, _ := telatest.AriaSnapshot(list)
fmt.Println(yaml)
// Output:
// - list:
// - listitem: Write docs
// - listitem: Ship it
func MapIndex[T any](items []T, f func(i int, item T) tela.Node) tela.Node

MapIndex is Map with the item’s index as the first argument of f: for numbering, alternating styles and the first or last item. The same rules apply to keys: use an ID, not the index, when the list changes.

MapIndex(steps, func(i int, s Step) tela.Node {
return Div(Key(s.ID), Textf("%d. %s", i+1, s.Title))
})

Example

steps := []string{"Mount", "Click", "Expect"}
list := Div(Role("list"),
MapIndex(steps, func(i int, s string) tela.Node {
return Div(Key(s), Role("listitem"), Textf("%d. %s", i+1, s))
}),
)
yaml, _ := telatest.AriaSnapshot(list)
fmt.Println(yaml)
// Output:
// - list:
// - listitem: 1. Mount
// - listitem: 2. Click
// - listitem: 3. Expect
func MapKeyed[T any, K comparable](items []T, key func(T) K, render func(T) tela.Component) tela.Node

MapKeyed returns the components of render applied to every item, as one keyed list: each is keyed by key(item), so reordering, inserting and removing items keeps the state of the component of every item that stays. It replaces Map(items, func(t T) tela.Node { return tela.WithKey(tela.CP(…), t.ID) }): render returns a node or a struct component, which MapKeyed keys (tela.WithKey), and K is any comparable type, so a slice or a map as a key does not compile. Dev mode warns about duplicate keys, once per call site, as it does for Map.

MapKeyed(todos, func(t Todo) int { return t.ID }, func(t Todo) tela.Component {
return TodoRow{Todo: t}
})

A render that wants a function component returns TodoRow(t), a function that returns tela.CP(todoRow, t). The key of a node that render keys itself, with Key, is replaced by key(item).

Example

t := &exampleT{}
defer t.close()
var set tela.Setter[[]todoItem]
list := func() tela.Node {
todos, s := tela.UseState([]todoItem{{1, "Write"}, {2, "Test"}})
set = s
return Div(MapKeyed(todos, func(t todoItem) int { return t.ID }, func(t todoItem) tela.Component { return t }))
}
app := telatest.Mount(t, tela.C(list))
app.Click(telatest.ByRole("checkbox", "Test")) // checks "Test"
// A new item arrives first: "Test" keeps its state.
set.Set([]todoItem{{3, "Plan"}, {1, "Write"}, {2, "Test"}})
fmt.Println(app.AriaSnapshot())
// Output:
// - checkbox "Plan"
// - checkbox "Write"
// - checkbox "Test" [checked]
func MaxLength(n int) tela.Prop

MaxLength limits what the user can type or paste into a text field to n UTF-16 code units (HTML’s maxlength: “😀” counts 2); a value set by the app may be longer. 0 means no limit.

func Name(s string) tela.Prop

Name names a text field or a button in its form (HTML’s name).

func Nodes(ns []tela.Node) tela.Node

Nodes returns the nodes of ns as one fragment, for a slice built in a loop that is given as children (Go does not spread a []tela.Node into the children of Div(…)). The nodes keep their order and, like the children of Fragment, have no box of their own. A node slice that comes from data and changes is a list: key it with Map or MapKeyed.

var cells []tela.Node
for _, c := range columns {
cells = append(cells, Div(Class("px-3"), Text(c)))
}
Div(Class("flex"), Nodes(cells))

Example

var cells []tela.Node
for _, name := range []string{"Name", "Role", "Team"} {
cells = append(cells, Div(Key(name), Class("px-3"), Text(name)))
}
// A slice cannot be spread into Div(...): Nodes makes it one child.
header := Div(Class("flex border-b"), Nodes(cells))
yaml, _ := telatest.AriaSnapshot(header)
fmt.Println(yaml)
// Output: - text: Name Role Team
func OnBeforeInput[F ~func() | ~func(tela.InputEvent)](handler F) tela.Prop

OnBeforeInput calls handler before a text field’s value changes by an edit of the user (the DOM’s beforeinput); e.PreventDefault() cancels the edit (except an IME composition’s). It bubbles.

func OnBlur[F ~func() | ~func(tela.FocusEvent)](handler F) tela.Prop

OnBlur calls handler when the element or one of its descendants loses the focus (React’s onBlur: it bubbles), also when the window loses the keyboard focus. Unmounting the focused element does not call it.

Div(TabIndex(0), OnBlur(func() { commit() }))
func OnChange[F ~func() | ~func(tela.ChangeEvent)](handler F) tela.Prop

OnChange calls handler after every edit of a text field’s value (React’s onChange: each keystroke, paste, cut, undo, and the text an IME composes, with e.IsComposing). It bubbles: on a Form it sees every field.

Input(Value(name), OnChange(func(e tela.ChangeEvent) { setName.Set(e.Value) }))
func OnClick[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnClick calls handler when the element is clicked: the primary button is pressed and released over it or its descendants (the click goes to the nearest element that contains both the press and the release, as in the DOM), even if the pointer moved in between, unless the press became a drag or a long press. The event bubbles to the ancestors.

Button(OnClick(func() { setCount.Update(func(n int) int { return n + 1 }) }), Text("+1"))
Button(OnClick(func(e tela.MouseEvent) { menu.At(e.X, e.Y) }), Text("Open"))

Every handler prop of this package (OnClick, OnKeyDown, OnChange, OnSubmit…) takes a func() or a func of its event type (or a named type of either, such as type ClickHandler func(tela.MouseEvent)): write the event only when the handler reads it. A nil handler panics.

To keep a click from reaching the ancestors’ OnClick (a delete button inside a clickable row), wrap the function in tela.StopPropagation, which every handler prop accepts: it calls e.StopPropagation() and then the function.

Button(OnClick(tela.StopPropagation(ctrl.Remove)), Text("Delete"))

Example

counter := func() tela.Node {
n, setN := tela.UseState(0)
return Div(
// Without the event: most handlers only set state.
Button(OnClick(func() { setN.Update(func(n int) int { return n + 1 }) }), Text("Add")),
// With it, when the handler reads it.
Button(OnClick(func(e tela.MouseEvent) { setN.Set(int(e.X)) }), Text("At x")),
Textf("n=%d", n))
}
t := &exampleT{}
defer t.close()
app := telatest.Mount(t, tela.C(counter), telatest.Size(300, 100))
app.Click(telatest.ByRole("button", "Add"))
app.Click(telatest.ByRole("button", "Add"))
app.ExpectText("n=2")
fmt.Println(app.AriaSnapshot())
// Output:
// - button "Add"
// - button "At x"
// - text: n=2
func OnCompositionEnd[F ~func() | ~func(tela.CompositionEvent)](handler F) tela.Prop

OnCompositionEnd calls handler when an input method ends a composition, committed (e.Data) or canceled (“”). It bubbles.

func OnCompositionStart[F ~func() | ~func(tela.CompositionEvent)](handler F) tela.Prop

OnCompositionStart calls handler when an input method (IME) starts composing text in a text field. It bubbles.

func OnCompositionUpdate[F ~func() | ~func(tela.CompositionEvent)](handler F) tela.Prop

OnCompositionUpdate calls handler when the text an input method composes changes (e.Data). It bubbles.

func OnContextMenu[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnContextMenu calls handler when the secondary (right) button is released over the element.

Div(OnContextMenu(func(e tela.MouseEvent) { openMenu(e.X, e.Y) }))
func OnDoubleClick[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnDoubleClick calls handler on the second click of a double click: two clicks within the OS’s double-click time (the Windows setting; 500 ms where the OS does not report one) and 5 px, as for double clicks on a title bar Tela draws; 300 ms and 100 px for touch. The first click is not delayed: OnClick runs for both, with e.ClickCount 1 and 2.

Div(OnDoubleClick(func() { setEditing.Set(true) }), Text(title))
func OnDrag[F ~func() | ~func(tela.DragEvent)](handler F) tela.Prop

OnDrag calls handler for every move of a drag (see OnDragStart), with the movement since the previous one (DeltaX, DeltaY) and since the press (OffsetX, OffsetY). Moves are batched: state set here is applied once per frame.

Div(OnDrag(func(e tela.DragEvent) { pos.Update(func(p float64) float64 { return p + float64(e.DeltaX) }) }))
func OnDragEnd[F ~func() | ~func(tela.DragEvent)](handler F) tela.Prop

OnDragEnd calls handler when a drag ends: the button is released (or the press cancelled). VelocityX and VelocityY give the release velocity in pixels per second, for flings.

Div(OnDragEnd(func(e tela.DragEvent) {
if e.VelocityX > 800 {
dismiss()
}
}))
func OnDragStart[F ~func() | ~func(tela.DragEvent)](handler F) tela.Prop

OnDragStart calls handler when a press on the element becomes a drag: the pointer moved more than a few pixels (4 for a mouse, 18 for touch and pens) before a long press. The press then does not click. Drag events go to the innermost element with drag handlers and do not bubble.

pos := tela.UseValue(0.0)
start := tela.UseRef(0.0)
Div(Class("size-10 rounded-full bg-indigo-500"),
Style(tela.TranslateFn(func() (x, y float32) { return float32(pos.Get()), 0 })),
OnDragStart(func() { start.Current = pos.Get() }),
OnDrag(func(e tela.DragEvent) { pos.Set(start.Current + float64(e.OffsetX)) }),
)
func OnFocus[F ~func() | ~func(tela.FocusEvent)](handler F) tela.Prop

OnFocus calls handler when the element or one of its descendants gains the focus (React’s onFocus: it bubbles), and again when the window regains the keyboard focus.

Div(TabIndex(0), OnFocus(func() { setActive.Set(true) }))
func OnKeyDown[F ~func() | ~func(tela.KeyboardEvent)](handler F) tela.Prop

OnKeyDown calls handler when a key is pressed (and again for each repeat while it is held, with e.Repeat) while the element or one of its descendants has the focus; the event bubbles. With nothing focused, key events go to the outermost element of the window’s content. e.PreventDefault() cancels the key’s default action: the focus move of Tab, the click of Enter or Space on a focused Button.

Div(OnKeyDown(func(e tela.KeyboardEvent) {
if e.Key == "Escape" {
close()
}
if e.Ctrl && e.Code == tela.CodeKeyS {
save()
}
}))
func OnKeyUp[F ~func() | ~func(tela.KeyboardEvent)](handler F) tela.Prop

OnKeyUp calls handler when a key is released while the element or one of its descendants has the focus; the event bubbles. Keys held when the window loses the focus are released then. e.PreventDefault() cancels the click of Space on a focused Button.

Div(OnKeyUp(func(e tela.KeyboardEvent) {
if e.Key == tela.KeyShift {
setMulti.Set(false)
}
}))
func OnLongPress[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnLongPress calls handler when the primary button (or a touch) stays pressed on the element for 500 ms without moving more than a few pixels (4 for a mouse, 18 for touch and pens). The press then does not click or drag. It goes to the innermost element with OnLongPress and does not bubble.

Div(OnLongPress(func(e tela.PointerEvent) { openMenu(e.X, e.Y) }), Text(name))
func OnMouseDown[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnMouseDown calls handler when a button is pressed over the element.

Div(OnMouseDown(func(e tela.MouseEvent) {
if e.Button == tela.MouseButtonMiddle {
closeTab()
}
}))
func OnMouseEnter[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnMouseEnter calls handler when the pointer enters the element or one of its descendants from outside. It does not bubble.

Div(OnMouseEnter(func() { setShowTip.Set(true) }))
func OnMouseLeave[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnMouseLeave calls handler when the pointer leaves the element and its descendants. It does not bubble.

Div(OnMouseLeave(func() { setShowTip.Set(false) }))
func OnMouseMove[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnMouseMove calls handler when the pointer moves over the element. Moves are batched: state set here is applied once per frame.

Div(OnMouseMove(func(e tela.MouseEvent) { pos.Set(e.LocalX) }))
func OnMouseUp[F ~func() | ~func(tela.MouseEvent)](handler F) tela.Prop

OnMouseUp calls handler when a button is released over the element.

Div(OnMouseUp(func() { setPressed.Set(false) }))
func OnPointerCancel[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerCancel calls handler when the system takes a pointer away during a press (a touch turned into a system gesture, a lost grab): treat it as an aborted press, not a release.

Div(OnPointerCancel(func() { discardStroke() }))
func OnPointerDown[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerDown calls handler when a pointer (mouse, touch or pen) is pressed over the element: the first button of a mouse, the contact of a touch. While a touch or pen is down its events go to this element. e.PreventDefault() suppresses the mouse events of the press (not the click).

Div(OnPointerDown(func(e tela.PointerEvent) {
if e.Kind == tela.PointerPen {
startStroke(e.LocalX, e.LocalY)
}
}))
func OnPointerEnter[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerEnter calls handler when a pointer enters the element or one of its descendants. It does not bubble.

Div(OnPointerEnter(func(e tela.PointerEvent) { setHoverKind.Set(e.Kind) }))
func OnPointerLeave[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerLeave calls handler when a pointer leaves the element and its descendants. It does not bubble.

Div(OnPointerLeave(func() { setHovered.Set(false) }))
func OnPointerMove[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerMove calls handler when a pointer moves over the element (or, for a touch or pen that pressed it, anywhere), and when a button is pressed or released while another is held.

Div(OnPointerMove(func(e tela.PointerEvent) { addPoint(e.LocalX, e.LocalY, e.Pressure) }))
func OnPointerUp[F ~func() | ~func(tela.PointerEvent)](handler F) tela.Prop

OnPointerUp calls handler when the last button of a pointer is released over the element.

Div(OnPointerUp(func() { endStroke() }))
func OnScroll[F ~func() | ~func(tela.ScrollEvent)](handler F) tela.Prop

OnScroll calls handler after the element’s scroll offsets changed, by the user or by code: at most once per frame, on the scrolling element itself (it does not bubble), with its offsets and sizes. To tie a style to the scroll position without rebuilding, use tela.UseScroll instead.

Div(Class("h-96 overflow-y-auto"), OnScroll(func(e tela.ScrollEvent) {
setAtTop.Set(e.ScrollTop == 0)
}), rows)
func OnSelect[F ~func() | ~func(tela.SelectEvent)](handler F) tela.Prop

OnSelect calls handler when the user selects text in a text field, or code changes its selection (e.Start, e.End). It bubbles.

func OnSubmit[F ~func() | ~func(tela.SubmitEvent)](handler F) tela.Prop

OnSubmit calls handler when a Form is submitted: Enter in one of its single-line fields, or a click on a submit Button inside it. It bubbles.

func OnWheel[F ~func() | ~func(tela.WheelEvent)](handler F) tela.Prop

OnWheel calls handler when a mouse wheel turns or a touchpad scrolls over the element (the DOM’s wheel event). The event bubbles. Unless a handler calls e.PreventDefault(), the nearest scrolling element (with overflow-auto or overflow-scroll) that can move in that direction scrolls, chaining to its scrolling ancestors at its edges unless it has overscroll-contain.

Div(OnWheel(func(e tela.WheelEvent) {
if e.Ctrl {
e.PreventDefault()
zoomBy(-e.DeltaY)
}
}))
func Overscan(n int) tela.Prop

Overscan sets how many items a VirtualList builds beyond each side of the visible ones: 5 by default. Tela builds a new window before painting the frame, so the overscan is not needed to hide blank areas while scrolling fast; it makes most scroll frames paint-only (the window moves only when the visible items reach its edge) and keeps the next items ready for Tab and the arrow keys. More items cost more layout and paint per frame; 0 rebuilds the list on every scroll that shows a new item.

VirtualList(len(rows), estimate, render, Overscan(10))
func P(children ...tela.Child) tela.Node

P returns a paragraph of text and inline elements (Span, Strong, Em, Code, A).

func Placeholder(s string) tela.Prop

Placeholder sets the text a text field shows while it is empty, in the field’s color at half opacity unless placeholder: classes style it.

func Portal(children ...tela.Child) tela.Node

Portal shows its children in a layer above everything else in the window, like React’s createPortal into document.body: use it for modals, drawers, menus, tooltips and toasts that must not be clipped or covered by the boxes around the component that renders them.

The layer covers the window and is laid out against it, so fixed and absolute content inside it positions against the window (Div(Class(“fixed inset-0 …”))). It is a stacking context of its own above the window’s content, whatever its z-index; portals mounted later are above earlier ones. The layer itself is never hit: clicks on its empty parts reach the content under it.

Everything else follows the component tree, as in React: the children keep their component state, read the context of the Portal’s ancestors, inherit their text style (color, font) from the Portal’s parent element, and their events bubble to the Portal’s ancestors (an OnClick on a Div around the Portal receives clicks from inside it). Tab order and the aria snapshot place the portal’s content after the window’s content, like content appended to the body.

Portal takes children and a Key; other props panic (style its content).

func Confirm(onClose func()) tela.Node {
return Portal(
Div(Class("fixed inset-0 flex items-center justify-center bg-black/40"),
Div(Role("dialog"), AriaLabel("Confirm"), Class("rounded-xl bg-white p-6 shadow-xl"),
Text("Delete this file?"),
Button(OnClick(func() { onClose() }), Text("Cancel")))))
}

Example

// The dialog is rendered by the card's component, but shown above
// the page; its content comes after the page's, as if appended to the
// body.
page := Div(
Div(Class("relative z-10 overflow-hidden rounded-xl p-4"),
Text("Card"),
Portal(Div(Class("fixed inset-0 flex items-center justify-center bg-black/40"),
Div(Role("dialog"), AriaLabel("Confirm"), Class("rounded-xl bg-white p-6"),
Button(Text("OK"))))),
),
Button(Text("Footer")),
)
yaml, _ := telatest.AriaSnapshot(page)
fmt.Println(yaml)
// Output:
// - text: Card
// - button "Footer"
// - dialog "Confirm":
// - button "OK"
func Presence(children ...tela.Child) tela.Node

Presence keeps a removed child mounted until its exit animation ends, then unmounts it (Motion’s AnimatePresence). Children are matched by key (or by position, which h.If keeps): when a build no longer has a child, the child exits if it has something to animate, its Exit classes or a component that uses tela.UsePresence; a child without either goes at once.

Presence(
If(open, Div(Key("dialog"), Role("dialog"),
Class("rounded-xl bg-white p-6 shadow-xl transition duration-200 ease-out "+
"opacity-100 scale-100 starting:opacity-0 starting:scale-95"),
Exit("opacity-0 scale-95"),
Text("Saved"))),
)

The entrance is the starting: variant (CSS @starting-style), which works on any element that starts being rendered, inside a Presence or not: its first style is computed with the starting: classes, and its transitions run from it to its normal style. The exit applies the Exit classes on top of the element’s classes, so the same transitions run to them; the child unmounts when they end (at once with reduced motion or without a transition).

An exiting child keeps its place in the layout (Motion’s default, not popLayout): position it absolutely, or inside an overlay, when the content around it should not wait. It shows its last build: the parent’s builds do not update it, but its own components still build. It is inert while it exits: the pointer goes through it to what is below, it cannot take the focus (focus inside it is lost and its focus scopes end, as on unmount), and it is gone from the semantics tree, so aria snapshots and telatest locators no longer see it. Adding the child back while it exits cancels the exit: the transitions run back from where they are, as in Motion.

Several children can exit at once, each at its old position. A Presence has no box: its children are children of its parent. It takes only Key as a prop. A nested Presence’s children do not exit with an outer child; they unmount with it.

Example

t := &exampleT{}
defer t.close()
app := telatest.Mount(t, tela.C(dialogDemo), telatest.NoSettle())
app.Click(telatest.ByRole("button", "Open"))
app.Advance(300 * time.Millisecond) // the entrance is over
app.Click(telatest.ByRole("button", "Close"))
app.Advance(100 * time.Millisecond)
// Halfway out the dialog is still drawn, but inert: gone for the
// semantics tree, the pointer and the focus. (Count settles the app,
// which ends the exit; the dialog then unmounts.)
fmt.Println(app.Count(telatest.ByRole("dialog", "")))
// Output: 0
func Range[R ~func(int) tela.Node | ~func(int) tela.Component](n int, render R) tela.Node

Range returns render(0) to render(n-1) as one list keyed by index: n items with no data behind them, such as skeleton rows, a star rating or the cells of a fixed grid. The index is the key, so no missing-key warning is given (a static range does not reorder), and a node that render keys itself is keyed by its index instead. render returns a tela.Node or a tela.Component, so a struct component needs no Fragment. n is 0 or more: a negative n panics in dev mode and renders nothing otherwise.

Range(5, func(i int) tela.Node { return Span(Textf("%d", i)) })
Range(5, func(i int) tela.Component { return Star{Filled: i < rating} })

For data that changes, use MapKeyed with the item’s ID: an index as the key moves the state of a component to the wrong item when items are inserted or removed.

func ReadOnly(on bool) tela.Prop

ReadOnly makes a text field read-only: it can be focused, selected and copied, but not edited; it matches read-only:.

func Ref(ref tela.ElementRef) tela.Prop

Ref attaches ref, from tela.UseElementRef, to the element: while it is mounted, ref.Focus(), ref.Blur(), ref.Focused() and the scroll methods (ref.ScrollTo, ScrollBy, ScrollIntoView, ScrollOffset) act on it, and tela.UseScroll(ref) follows its scroll offsets. It panics on the zero ElementRef.

input := tela.UseElementRef()
tela.UseMount(func() func() {
input.Focus()
return nil
})
return Div(Ref(input), TabIndex(0), Class("focus:ring-2"), Text("Name"))
func Role(role string) tela.Prop

Role sets the ARIA role of an element (WAI-ARIA 1.2), for components built from divs: Role(“switch”), Role(“dialog”), Role(“tab”). An unknown role panics in dev mode (on in development builds, see tela.App.Dev) with the closest valid one.

Button(Role("switch"), AriaChecked(on), AriaLabel("Wi-Fi"),
OnClick(func() { setOn.Set(!on) }), knob)

Example

enabled := true
toggle := Button(Role("switch"), AriaChecked(enabled), AriaLabel("Wi-Fi"),
Class("h-6 w-11 rounded-full bg-indigo-600"))
yaml, _ := telatest.AriaSnapshot(toggle)
fmt.Println(yaml)
// Output:
// - switch "Wi-Fi" [checked]
func Rows(n int) tela.Prop

Rows sets how many lines tall a Textarea is (2 by default).

func Scale(v *tela.Value[float32], k float32) func() float32

Scale returns a function that reads v and multiplies it by k, for the deferred style props that take a func() float32 (tela.WidthFn, tela.HeightFn, tela.OpacityFn): the Value is tracked where the prop is evaluated, not when the component builds. It replaces the closure func() float32 { return 36 * v.Get() }; tela.MapValue is the general form.

open := tela.UseSpring(target) // a *tela.Value[float32] from 0 to 1
Div(Class("overflow-hidden"), Style(tela.HeightFn(Scale(open, 36))))

Example

t := &exampleT{}
defer t.close()
var open *tela.Value[float32]
section := func() tela.Node {
open = tela.UseValue(float32(0))
// 36 px closed, 72 px at 1: the box is laid out again when open
// changes; the component does not build.
return Div(TestID("body"), Class("w-40 overflow-hidden bg-slate-100"), Style(tela.HeightFn(Scale(open, 72))))
}
app := telatest.Mount(t, tela.C(section))
open.Set(0.5)
app.Advance(32 * time.Millisecond)
fmt.Println(app.Bounds(telatest.ByTestID("body")).Height)
// Output: 36
func Scheme(s tela.ColorScheme) tela.Prop

Scheme gives an element and everything inside it a color scheme of its own, tela.Light or tela.Dark, whatever the app’s scheme: their dark: classes and theme colors (bg-card, text-foreground; see tela.Theme) follow it, and so do tela.UseColorScheme and tela.UseTheme in the components below. A nested Scheme wins over an outer one. tela.System removes the override (the element follows its parent).

// A dark sidebar in a light app.
Div(Scheme(tela.Dark), Class("w-64 bg-background text-foreground border-r border-border"),
Sidebar(),
)

Content in an h.Portal follows the scheme of the Portal’s position. It panics for other values.

func Span(children ...tela.Child) tela.Node

Span returns an inline container. Inside a paragraph of text it styles a run of that text (color, weight…) and is a CSS inline box: its background, border, rounded corners and horizontal padding and margins apply on every line it is on (Class(“rounded bg-slate-100 px-1”)). With Class(“inline-block”) or “inline-flex” it is laid out as a box that sits on the line. As a child of a flex container it is a flex item.

func SpellCheck(on bool) tela.Prop

SpellCheck is HTML’s spellcheck hint. It is accepted and ignored: Tela has no spell checker.

func Src(img image.Image) tela.Prop

Src sets the image of an Img. Pass the same image.Image value on every build: it is converted for drawing only when another image is given.

func SrcBytes(data []byte) tela.Prop

SrcBytes sets the image of an Img to encoded data: PNG, JPEG, GIF (the first frame), WebP or SVG, recognized by its content. The data is decoded once and cached by its content; passing the same slice on every build costs a comparison. Do not modify the slice afterwards.

func SrcFS(fsys fs.FS, path string) tela.Prop

SrcFS sets the image of an Img to file path of fsys (an embed.FS, an os.DirFS…), decoded once and cached by file system and path, like SrcPath but without registering fsys with tela.Assets.

func SrcPath(path string) tela.Prop

SrcPath sets the image of an Img to a file of the file systems registered with tela.Assets, resolved like the url() of bg-[url(img/a.png)]: searched in registration order, relative to their roots (a leading “/” or “./” is ignored, “..” is not allowed). The file is decoded once, when first shown, and cached for the life of the process; its format is recognized by its content: PNG, JPEG, GIF (the first frame), WebP or SVG. A missing or undecodable file shows nothing and is reported once in the dev log.

//go:embed img
var images embed.FS
func init() { tela.Assets(images) }
Img(SrcPath("img/logo.svg"), Class("h-8"))

Example

t := &exampleT{}
defer t.close()
// Usually an embed.FS registered from an init function.
tela.Assets(fstest.MapFS{"img/logo.svg": {Data: []byte(
`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#4f46e5"/></svg>`)}})
// With only a height, the width follows the image's aspect ratio.
app := telatest.Mount(t, Img(SrcPath("img/logo.svg"), Alt("Logo"), Class("h-8")))
r := app.Bounds(telatest.ByRole("img", "Logo"))
fmt.Println(r.Width, r.Height)
// Output: 96 32
func Strong(children ...tela.Child) tela.Node

Strong returns bold inline text.

func Style(props ...tela.StyleProp) tela.Prop

Style adds typed style props from package tela, for values computed in code:

Style(tela.Width(w), tela.Bg(tela.Hex("#1e1e2e")))

They apply after the element’s classes and win over them. A tela.StyleProp is also a child, so an element takes one directly, which is the same:

Div(tela.Width(w), tela.Bg(tela.Hex("#1e1e2e")))

Example

// Typed style props come from package tela and win over classes.
swatch := Div(Class("size-16 bg-red-500"), Style(tela.Bg(tela.RGB(0, 128, 255))))
img, _ := telatest.Render(swatch, telatest.Size(64, 64))
fmt.Println(img.RGBAAt(32, 32))
// Output:
// {0 128 255 255}
func TabIndex(i int) tela.Prop

TabIndex makes an element focusable (a Button already is) and places it in the Tab order: 0 in tree order, -1 out of it (focusable by click and by ElementRef.Focus only). A positive index puts the element before every 0, in ascending order; avoid it, since the Tab order then no longer follows the layout. Disabled, display: none and invisible elements are not focusable; the focused element is blurred when it becomes one of them.

Div(TabIndex(0), Class("rounded-lg p-4 focus-visible:ring-2 focus-visible:ring-indigo-500"),
OnKeyDown(func(e tela.KeyboardEvent) {
if e.Key == tela.KeyEnter {
open()
}
}),
Text("Inbox"))
func TestID(id string) tela.Prop

TestID names an element for tests, like the data-testid attribute: find it with telatest.ByTestID, which resolves the name to exactly the element that carries it, whatever its role, text or classes. It changes nothing on screen and is not exposed anywhere else: not in the semantics tree, the aria snapshot or the platform’s accessibility API, so it never leaks into what assistive technologies or tela inspect show.

Div(TestID("sidebar"), Class("w-64"), ...)
// in the test:
app.Click(telatest.ByTestID("sidebar"))

Use it for elements a user cannot name (a card, a chart, a drop zone); prefer telatest.ByRole and telatest.ByText when the element has a role or text, since those tests also check what users see.

func Text(s string) tela.Node

Text returns a text node. The text is laid out in the paragraph of its nearest block ancestor and styled by its parent element.

func TextFn(fn func() string) tela.Node

TextFn returns a text node whose content is read by fn in a later phase, not when the component builds: a change of a tela.Value that fn read lays out only the paragraph that holds the text, and no component builds. It is Text for what changes every frame, such as a clock or a time label:

pos := tela.UseValue(0.0) // seconds, set from a ticker
Span(Class("tabular-nums"), TextFn(func() string { return clock(pos.Get()) }))

It lays out like Text, inside paragraphs next to other text and inline elements. fn runs on the UI thread, when the paragraph is assembled (and the Values it read changed): it should only compute the string. A closure that captures the component’s state is replaced by the new one whenever the component builds.

func Textarea(children ...tela.Child) tela.Node

Textarea returns a multi-line text field: like Input, but Enter inserts a line break, its text wraps, and it scrolls vertically (overflow auto) when the text is taller than it. With text-nowrap (or whitespace-pre) its lines do not wrap, and it also scrolls horizontally when a line is wider than it. It is Rows lines tall (2 by default) and about 20 characters wide unless classes size it; field-sizing-content makes it grow with its text (between min-h-* and max-h-*). It takes no children: give its text with Value, DefaultValue or BindValue.

Textarea(Rows(4), Placeholder("Write a message"), Bind(msg, setMsg),
Class("w-full rounded-md border border-slate-300 p-2"))
func Textf(format string, args ...any) tela.Node

Textf returns a text node formatted like fmt.Sprintf.

func Tint(c tela.Color) tela.Prop

Tint paints the image of an Img in color c through its alpha: each pixel becomes c with the image’s opacity (the shading is lost), for raster and SVG sources alike. It is how monochrome PNG icons take a color:

Img(SrcPath("icons/claude.png"), Tint(tela.Hex("#D97757")), Class("size-5"))

An element of any kind can do the same with a CSS mask: Div(Class(“size-5 bg-[#D97757] mask-[url(icons/claude.png)] mask-contain”)).

Example

t := &exampleT{}
defer t.close()
// A monochrome icon: only its alpha matters.
glyph := `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#000"/></svg>`
app := telatest.Mount(t, Img(SrcBytes([]byte(glyph)), Tint(tela.Hex("#D97757")), Class("size-8")))
fmt.Println(app.PixelAt(16, 16))
// Output: {217 119 87 255}
func Type(t string) tela.Prop

Type sets the type of an Input: “text” (the default), “password” (the value shows as dots and cannot be copied), “email”, “search”, “url”, “tel” or “number” (a hint for input methods; the value stays text, with no spinner). “email” and “url” values that are not valid match invalid:, unless the Input is ReadOnly or disabled (HTML bars those from validation). On a Button it sets “submit” (the default inside a Form), “button” or “reset”. An unknown type panics in dev mode.

func Value(s string) tela.Prop

Value makes a text field controlled: it shows s, and after every event it shows s again unless the app rebuilt it with a new value (React’s controlled inputs). Pair it with OnChange, or use Bind.

func VirtualList[R ~func(int) tela.Node | ~func(int) tela.Component](count int, estimateSize func(i int) float32, renderItem R, props ...tela.Prop) tela.Node

VirtualList is a vertical scrolling list of count items that builds, lays out and paints only the items in view, plus a few more on each side (see Overscan), so a list of 100,000 rows costs about as much as one screenful of them (TanStack Virtual’s model). The node is the scrolling element itself: props style it like a Div (Class, Style, Ref, handlers, Key, Role and the Aria props), and it is overflow-y-auto unless a class sets another overflow. Give it a height (h-96, flex-1 in a column, h-screen): the items it builds are bounded by what the window shows, but only a list that scrolls by itself keeps its position stable and supports ScrollToIndex.

estimateSize(i) estimates the height of item i in logical pixels, before it is built; renderItem(i) returns it, as a tela.Node or as a tela.Component (renderItem is a func(i int) tela.Node or a func(i int) tela.Component, so a struct component row needs no wrapper). Each item is measured once it is laid out, and the measurement replaces the estimate (per index; a new estimateSize function resets them), so items may have any height and the scrollbar, OnScroll’s ScrollHeight and the aria tree reflect the whole list. When a measurement changes the size of an item above the top of the list’s viewport, the scroll offset moves by the difference in the same frame: the content on screen does not jump.

Items are keyed by their index unless ItemKey says otherwise: an item component’s state follows its index, so give the list ItemKey when items are inserted, removed or reordered, and the state must follow the item. For rows of one height, ItemHeight sets it, skips measuring and makes estimateSize unnecessary (it may be nil). The list has the role “list” and its items “listitem”, for the semantics tree and the aria snapshot; Role on the list replaces its role (the items are then not listitems), and an item’s own role goes on the element renderItem returns. renderItem runs inside the list’s build, so it must not call hooks; return a component (a struct component such as Row{Item: it}, or a node from tela.C or tela.CP) for items with state, so that a list rebuilt by its parent skips the items whose props did not change. Scrolling never rebuilds the items that stay in view: a scroll inside the built items only repaints, and a scroll past them builds the entering items and unmounts the leaving ones. The focused item stays mounted while it is scrolled away, so Tab goes on from it.

Scroll to an item with the ref’s ScrollToIndex (tela.AlignAuto, AlignStart, AlignCenter, AlignEnd), which keeps the item aligned while the items around it are measured, and which jumps or animates like ScrollTo (scroll-smooth):

list := tela.UseElementRef()
return Div(Class("flex h-screen flex-col"),
Button(OnClick(func() { list.ScrollToIndex(5000, tela.AlignCenter) }),
Text("Go to 5000")),
VirtualList(100_000,
func(int) float32 { return 40 },
func(i int) tela.Component { return Row{Index: i} },
Ref(list), Class("flex-1 rounded-lg border")),
)

Lists are vertical; horizontal lists and grids are not virtualized yet.

Example

// A million rows cost about as much as the ones in view.
rows := VirtualList(1_000_000,
func(int) float32 { return 40 },
func(i int) tela.Node { return Div(Class("h-10"), Textf("Row %d", i)) },
Class("h-[120px]"), Overscan(0))
yaml, _ := telatest.AriaSnapshot(rows, telatest.Size(200, 120))
fmt.Println(yaml)
// Output:
// - list:
// - listitem: Row 0
// - listitem: Row 1
// - listitem: Row 2
func WindowControl(c tela.WindowControl) tela.Prop

WindowControl makes an element a window-control button of a custom title bar: a click on it minimizes the window (tela.WindowMinimize), maximizes or restores it (tela.WindowMaximize) or asks to close it (tela.WindowClose: the window’s OnClose runs, as with the OS’s close button). The OS also learns what the button is (Windows 11 shows its snap layouts over the maximize button). Give it a label for assistive technologies (AriaLabel).

Button(WindowControl(tela.WindowMaximize), AriaLabel("Maximize"), Icon(lucide.Square, Class("size-4")))
func WindowDrag() tela.Prop

WindowDrag makes an element a drag area of the window’s title bar, for a frameless window that draws its own (tela.App.Frameless, tela.Window.Frameless), or any window: pressing it and moving moves the window, a double click maximizes or restores it and a right click opens the window menu, as in the OS’s own title bar. Its interactive elements keep working: buttons, links, text fields, focusable elements and elements with press or drag handlers are left out of the drag area by themselves, and WindowNoDrag leaves out any other.

Div(Class("flex h-10 items-center gap-2 px-3"), WindowDrag(),
Span(Class("grow text-sm font-medium"), Text("Notes")),
Button(WindowControl(tela.WindowClose), AriaLabel("Close"), Text("×")),
)
func WindowNoDrag() tela.Prop

WindowNoDrag leaves an element out of the WindowDrag area around it: a press there reaches the element instead of moving the window. Elements that take presses (buttons, fields, links) need no WindowNoDrag; use it for a plain area that should not drag, such as a tab strip with its own gestures. A WindowDrag inside it is a drag area again.