Elements and Props
Package h holds the elements, props and helpers of a Tela UI. It is meant to
be dot-imported, so markup reads like JSX:
import ( "github.com/kuronosu/tela" . "github.com/kuronosu/tela/h")To keep collisions with your own package unlikely, h exports only element
constructors, props and a few helpers. Everything else (the app, hooks,
events, colors, typed style props) lives in package tela.
Building a tree
Section titled “Building a tree”An element takes its props and children in one list, in any order:
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?"))), )}- Props apply in order: a later
Classwins over an earlier one, andStylewins over every class. - Children are nodes and components: an element, a text, or a
tela.Componentvalue such asCard{Title: "Inbox"}(a struct with aBuildmethod, a component with its own state; see Hooks). The parameters aretela.Child, an interface (aBuild() tela.Nodemethod) that a barestringdoes not satisfy, so passing one is a compile error: wrap text inTextorTextf. The compiler names the internal type behind the alias, soDiv("hello")reads:cannot use "hello" (constant of type string) as tela.Child value in argument to Div: string does not implement core.Child (missing method Build). - Elements have the defaults of Tailwind’s Preflight: no margins, padding or borders, headings in the surrounding font size, block images. Style everything with classes.
Elements
Section titled “Elements”| Element | What it is |
|---|---|
Div |
A block container, the general-purpose box. Its children stack vertically unless classes say otherwise (flex) |
Span |
An inline container. Inside a paragraph it styles a run of text; as a flex child it is a flex item; with inline-block it sits on the line as a box |
Text, Textf |
A text node (Textf formats like fmt.Sprintf). It is laid out in the paragraph of its nearest block ancestor and styled by its parent |
H1 to H6 |
Headings, with their level in the semantics tree. Unstyled: size and weight come from classes |
P |
A paragraph of text and inline elements |
Button |
A button: no background, border or padding of its own, fits its content, shows the pointer cursor, focusable, clicks with Enter and Space |
Img |
An image: PNG, JPEG, GIF, WebP or SVG (vector) from SrcPath, SrcBytes, SrcFS or a decoded Src; without sizes it is drawn at its own size, shrunk to fit its container |
Icon |
An SVG icon (tela.Icon): 1em by 1em unless sized, colored by the text color |
Strong, Em, Code |
Inline bold, italic and monospace text |
A |
A link: inline text with a destination from Href. Unstyled; Tela does not open links by itself yet, handle OnClick |
Input, Textarea, Form, Label |
Text fields and forms: see Forms and Text Input |
VirtualList |
A scrolling list that builds only the rows in view: see Scrolling and VirtualList |
Rich text mixes inline elements in a paragraph; a Span with a background and
rounded corners is a CSS inline box that wraps across lines:
func Note() tela.Node { return P(Class("max-w-md leading-7 text-slate-700"), Text("Press "), Code(Class("rounded bg-slate-100 px-1"), Text("Esc")), Text(" to "), Strong(Text("close")), Text(" the "), A(Href("https://pkg.go.dev/github.com/kuronosu/tela"), Class("text-indigo-600 underline"), Text("dialog")), Text("."), )}Images
Section titled “Images”An image comes from a file of the file systems registered with
tela.Assets (the same lookup as bg-[url(...)]), from bytes, from
any fs.FS, or from an image.Image you decoded (pass the same value on
every build). The format is recognized by its content; SVG stays vector, so
it is sharp at every size and scale:
//go:embed imgvar images embed.FS
func init() { tela.Assets(images) }
func Header(avatar image.Image) tela.Node { return Div(Class("flex items-center gap-3"), Img(SrcPath("img/logo.svg"), Alt("Acme"), Class("h-8")), // the width follows the ratio Img(Src(avatar), Alt("Ada Lovelace"), Class("size-10 rounded-full object-cover")), )}Files are decoded once and cached (by path, by content for SrcBytes, by file
system and path for SrcFS); a missing or undecodable source shows nothing
and is reported once in the dev log. object-contain, object-cover,
object-none, object-scale-down and object-top-left, object-[25%_75%]…
fit the image in its box as in CSS. Tint paints a monochrome image in one
color through its alpha: Img(SrcPath("icons/claude.png"), Tint(tela.Hex("#D97757")), Class("size-5")).
Icon shows a tela.Icon, the text of an SVG document that can be
package-level data (var Check = tela.Icon{SVG: "<svg ...>"}, or
tela.MustSVG(data) for an embedded file). It is parsed once and cached by its
text. An icon is 1em by 1em, so it follows the font size like an icon font,
unless size-*, w-*/h-* or a style sizes it; it is a block, like an svg
under Preflight (Class("inline") puts it on a line of text). Its
currentColor is the text color, so text-* classes, tokens and transitions
recolor it, and fill-*, stroke-* and stroke-[1.5] (on the icon or on an
ancestor such as the button) override the document’s own paints:
Button(Class("inline-flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground"), Icon(Check, Class("size-4 stroke-[1.5]")), Text("Save"))Icons are decorative (hidden from accessibility) unless they have an
AriaLabel or AriaLabelledBy, which make them images with that name.
The Icons page covers the Lucide set (github.com/kuronosu/tela/icons/lucide),
generating a set from SVG files with tela icons gen, and the cost in a binary.
| Prop | Does |
|---|---|
Class |
Adds Tailwind classes |
Style |
Adds typed style props from package tela |
Key |
Gives an element an identity among its siblings |
Disabled |
Disables an element and its subtree for the pointer and the focus |
Scheme |
Gives an element and its subtree their own color scheme (tela.Light, tela.Dark): see Theming |
Dir |
Direction of an element and its subtree ("rtl", "ltr", "auto"), like HTML’s dir: see Internationalization |
Src, SrcPath, SrcBytes, SrcFS, Tint, Alt, Href |
Image sources, a tint, the image’s alternative text, a link’s destination |
ID, For, Name |
Identifier of an element, the field a label is for, name of a field in its form |
TabIndex, AutoFocus, Ref |
Focus order, focus on appearance, a handle on the element |
Role and the Aria... props |
Semantics: see Accessibility |
Value, DefaultValue, Bind, BindValue, Type, Rows, Placeholder, ReadOnly, MaxLength, AutoComplete, SpellCheck |
Text fields: see Forms and Text Input |
On... handlers |
See Events |
Overscan, ItemKey, ItemHeight |
How many extra items a VirtualList builds, what keys its items, and a fixed item height |
AnimateLayout, LayoutID, Exit |
Layout animations, shared-element transitions and exit classes under a Presence: see Animation |
When a list changes, the element with the same Key keeps its state wherever
it moves. The key is a comparable value, such as an item ID (not its index
when the list reorders): Key is generic, so a slice or a map does not
compile. Dev mode warns about a list of two or more nodes without keys and
about duplicate keys among siblings, once per call site of the Map that made
the list. A component or fragment cannot take the Key prop; use
tela.WithKey, which also keys a struct component and makes a node of it:
Map(cards, func(c CardData) tela.Node { return tela.WithKey(Card{Title: c.Title}, c.ID) })A component built with tela.C, tela.CP or tela.Memo takes the key as its
last argument: tela.CP(row, RowProps{...}, Key(row.ID)). The public wrapper
of a function component (func Row(p RowProps) tela.Node { return tela.CP(row, p) })
takes none; key where you use it with tela.WithKey(Row(p), row.ID). And for a
list of components MapKeyed does the keying:
MapKeyed(todos, func(t Todo) int { return t.ID }, func(t Todo) tela.Component { return TodoRow{Todo: t} // a struct component, or the wrapper of a function component})Helpers
Section titled “Helpers”If and IfElse
Section titled “If and IfElse”If returns a node or a prop when a condition holds, and the zero value
otherwise. The zero node renders nothing but keeps the positions of the
siblings after it, so toggling the condition does not remount them. It works
for props too. IfElse picks between two:
func Status(loading bool, err error, selected bool) tela.Node { return Div( If(loading, Span(Text("Loading..."))), If(selected, Class("ring-2 ring-indigo-500")), IfElse(loading, Class("opacity-50"), Class("opacity-100")), If(err != nil, P(Class("text-red-600"), Text(errorText(err)))), )}
func errorText(err error) string { if err == nil { return "" } return err.Error()}Every argument of a call is evaluated: If(user != nil, Text(user.Name))
dereferences a nil user. Use an ordinary if for that case.
If and IfElse return a tela.Child, so they take a node, a struct
component (If(loading, Spinner{})) or a prop (If(active, Class("ring-2"))),
and when the branch is absent they render and apply nothing. A function
component that returns one wraps it: return Fragment(If(...)).
Map and MapIndex
Section titled “Map and MapIndex”Map turns a slice into a list of nodes; MapIndex also gives the index. Give
each node a Key.
type Step struct { ID int Title string}
func Steps(steps []Step) tela.Node { return Div( MapIndex(steps, func(i int, s Step) tela.Node { return Div(Key(s.ID), Textf("%d. %s", i+1, s.Title)) }), )}Range(n, render) returns render(0) to render(n-1) as one list keyed by
index, for items with no data behind them (skeleton rows, a star rating); a
static range has no missing-key warning. render returns a tela.Node or a
tela.Component, so a struct component needs no Fragment:
Range(5, func(i int) tela.Component { return Star{Filled: i < rating} })For data that changes, key by the item’s identity with Map or MapKeyed.
MapKeyed and Nodes
Section titled “MapKeyed and Nodes”MapKeyed(items, key, render) is Map for components: it keys each
one by key(item), so reordering, inserting and removing keeps the state of
the others. render returns a node or a struct component; the key type is
any comparable type. Nodes(ns) passes a []tela.Node built in a loop as
one child (Go does not spread it into Div(...)); the nodes have no box of
their own, like those of Fragment.
var cells []tela.Nodefor _, c := range columns { cells = append(cells, Div(Class("px-3"), Text(c)))}Div(Class("flex"), Nodes(cells))Fragment and TextFn
Section titled “Fragment and TextFn”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 also how a
wrapper puts the children it receives next to its own, since Go does not allow
Div(Class("..."), H2(...), children...):
// 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...))}It puts several children in one field of type tela.Component
(Panel{Children: Fragment(Text("a"), Span(Text("b")))}), and makes a node of
a struct component where a tela.Node is expected
(return Fragment(Panel{Title: "Inbox"})).
TextFn is a text node
whose content is read in a later phase, so a change of a tela.Value it reads
lays out only the paragraph, without building any component; see
Hooks.
func Elapsed(seconds *tela.Value[float64]) tela.Node { return Span(Class("tabular-nums"), TextFn(func() string { return fmt.Sprintf("%.1f s", seconds.Get()) }))}Portals
Section titled “Portals”Portal shows its children in a layer above the whole window, like React’s
createPortal into document.body: the overlay is not clipped by
overflow-hidden ancestors and not covered by their z-index. The layer is laid
out against the window, so fixed inset-0 covers it and absolute content
positions against it; later portals are above earlier ones; clicks on the
layer’s empty parts reach what is under it.
Everything else follows the component tree: the content keeps its state, reads the context of the Portal’s ancestors, inherits their text color and font, and its events bubble to the Portal’s ancestors. Tab order and the aria snapshot put it after the window’s content.
func Menu() tela.Node { open, setOpen := tela.UseState(false) return Div(Class("relative overflow-hidden"), Button(OnClick(func(tela.MouseEvent) { setOpen.Set(!open) }), Text("Menu")), If(open, Portal( Div(Class("fixed inset-0"), OnClick(func(tela.MouseEvent) { setOpen.Set(false) })), Div(Role("menu"), Class("fixed top-12 left-4 rounded-lg bg-white p-2 shadow-xl"), Button(Role("menuitem"), Text("Rename"))), )), )}Presence
Section titled “Presence”Presence keeps a removed child mounted while it runs its
exit (the classes of its Exit prop, or a component’s own exit
through tela.UsePresence), then unmounts it. Enter animations need no
Presence: a starting: class gives a newly shown element the style it
transitions from. See Animation.
See also
Section titled “See also”- API-Reference-h lists every symbol with its doc comment.
- Styling for what goes in
Class.