Skip to content

Events

Events follow the DOM and React. A handler is a function that receives the event by value; the event’s methods control what happens next. Handlers are props of package h: OnClick, OnKeyDown, OnDrag…

func Toggle() tela.Node {
open, setOpen := tela.UseState(false)
return Div(Class("rounded-xl border p-4"),
OnClick(func(e tela.MouseEvent) {
e.StopPropagation() // an ancestor's OnClick does not run
setOpen.Set(!open)
}),
IfElse(open, Text("Open"), Text("Closed")),
)
}

Every handler prop panics when its handler is nil, with a message that shows the right call.

To stop an event from reaching the ancestors’ handlers without reading it, wrap the function in tela.StopPropagation, which every handler prop accepts:

Div(OnClick(openRow),
Button(OnClick(tela.StopPropagation(remove)), Text("Delete")), // openRow does not run
)
Div(OnKeyDown(tela.StopPropagation(closeDialog)))

It calls e.StopPropagation() and then the function. A handler that stops the event only sometimes takes the event and calls e.StopPropagation() itself. (The press hook of the aria package stops propagation by default, as React Aria does.)

  • A pointer event’s target is the topmost element under the pointer as painted, inside the clips and transforms of its ancestors, skipping pointer-events-none and invisible elements.
  • Events bubble from the target to the root, calling each ancestor’s handler. StopPropagation() stops that. OnMouseEnter, OnMouseLeave, OnPointerEnter, OnPointerLeave, OnLongPress, the drag handlers and OnScroll do not bubble.
  • A Disabled element and its descendants receive no pointer events.
  • PreventDefault() cancels Tela’s default action for the event (the focus move of Tab, the scroll of a wheel notch, the text a key types). Which default each event has is listed on its handler.
  • After every discrete event (presses, releases, clicks, drag start and end, long press), pending state updates are applied and components rebuilt before the next event, so two clicks before a frame see each other’s updates. Continuous events (moves, drag moves) are batched: state set there is applied once per frame.
Area Handlers Event
Mouse OnClick OnDoubleClick OnContextMenu OnMouseDown OnMouseUp OnMouseMove OnMouseEnter OnMouseLeave tela.MouseEvent
Pointer (mouse, touch, pen) OnPointerDown OnPointerMove OnPointerUp OnPointerCancel OnPointerEnter OnPointerLeave OnLongPress tela.PointerEvent
Drag OnDragStart OnDrag OnDragEnd tela.DragEvent
Keyboard OnKeyDown OnKeyUp tela.KeyboardEvent
Focus OnFocus OnBlur tela.FocusEvent
Wheel and scroll OnWheel OnScroll tela.WheelEvent, tela.ScrollEvent
Text fields and forms OnChange OnBeforeInput OnCompositionStart OnCompositionUpdate OnCompositionEnd OnSelect OnSubmit see Forms and Text Input

Every handler takes a func() or a func of its event type, so write the event only when the handler reads it (about four handlers in five do not):

Button(OnClick(func() { setCount.Update(func(n int) int { return n + 1 }) }), Text("+1"))
Div(OnMouseMove(func(e tela.MouseEvent) { pos.Set(e.LocalX) }))

A nil handler panics, naming both shapes. tela.Toggle(setOpen) is a ready-made func() that flips a bool state with Update, so two clicks that land in one frame both count: OnClick(tela.Toggle(setOpen)).

The fields and methods of the event types are listed in API-Reference-tela; the common ones are below.

Pointer events fire for every pointer: mouse, touch and pen. Mouse events are the compatibility view of the primary pointer, in the DOM order: pointerdown, mousedown, pointerup, mouseup, click.

Field Meaning
X, Y Position in the window, in logical pixels
LocalX, LocalY Position relative to the border box of the element whose handler runs
Button, Buttons The button that changed (tela.MouseButtonLeft, tela.MouseButtonRight, tela.MouseButtonMiddle…) and the set held; e.Buttons.Has(tela.MouseButtonLeft)
Shift, Ctrl, Alt, Meta Modifier keys (Meta is the Windows, Command or Super key)
ClickCount 1 for a click, 2 for the second click of a double click
Kind, PointerID, Pressure, IsPrimary On a PointerEvent: tela.PointerMouse, tela.PointerTouch or tela.PointerPen, and the pointer’s identity
func Tab(title string, closeTab func()) tela.Node {
return Div(Class("rounded-t-lg px-4 py-2 hover:bg-slate-100"),
OnMouseDown(func(e tela.MouseEvent) {
if e.Button == tela.MouseButtonMiddle {
closeTab()
}
}),
OnContextMenu(func(e tela.MouseEvent) { openMenu(e.X, e.Y) }),
Text(title),
)
}
func openMenu(x, y float32) {}

OnDoubleClick runs on the second click of a double click (two clicks within the OS’s double-click time and 5 px: the Windows setting, 500 ms where the OS does not report one; 300 ms and 100 px for touch); the first click is not delayed, so OnClick runs for both.

Key events go to the focused element and bubble; with nothing focused they go to the outermost element of the window’s content. A tela.KeyboardEvent has:

func Editor(save, closeEditor func()) tela.Node {
return Div(TabIndex(0), Class("p-4 outline-none"),
OnKeyDown(func(e tela.KeyboardEvent) {
switch {
case e.Key == tela.KeyEscape:
closeEditor()
case e.Ctrl && e.Code == tela.CodeKeyS:
e.PreventDefault()
save()
}
}),
Text("Press Ctrl+S to save, Esc to close"),
)
}

After the handlers, Tela runs the default actions unless one called PreventDefault: Tab and Shift+Tab move the focus through the focusable elements; Enter on a focused Button clicks it on keydown, Space on keyup. Typed text reaches text fields, which report it with OnChange.

A Button is focusable. Any other element becomes focusable with TabIndex: TabIndex(0) puts it in the Tab order in tree order, TabIndex(-1) makes it focusable by click and by code only. Disabled and hidden elements are not focusable. AutoFocus focuses an element when it first appears. A press of the primary button focuses the nearest focusable element under the pointer, and blurs the focused one otherwise.

OnFocus and OnBlur bubble, like React’s: a container’s OnFocus also runs when a descendant gains the focus, which makes “focus within” logic easy. The focus:, focus-visible: and focus-within: variants style the focused element.

func Combo() tela.Node {
editing, setEditing := tela.UseState(false)
return Div(
OnFocus(func(tela.FocusEvent) { setEditing.Set(true) }),
OnBlur(func(tela.FocusEvent) { setEditing.Set(false) }),
Class("rounded-lg border p-2 focus-within:ring-2 focus-within:ring-indigo-500"),
Input(AriaLabel("Name")),
If(editing, Span(Class("text-xs text-slate-500"), Text("editing"))),
)
}

To move the focus from code, attach an tela.ElementRef with Ref and call Focus(); see Hooks.

A press that moves farther than a few pixels (4 for a mouse, 18 for touch and pen) before a long press becomes a drag, and does not click. Drag events go to the innermost element with drag handlers. A tela.DragEvent carries DeltaX and DeltaY (since the previous event), OffsetX and OffsetY (since the press, so an element moved by them follows the pointer exactly) and VelocityX, VelocityY in pixels per second (meaningful at the end, for flings).

Moving an element by repainting only is the job of tela.TranslateFn with a UseValue; no component builds while the card follows the pointer:

type Offset struct{ X, Y float32 }
func DraggableCard() tela.Node {
offset := tela.UseValue(Offset{})
start := tela.UseRef(Offset{})
return Div(Class("w-48 rounded-xl bg-white p-6 shadow-lg"),
Style(tela.TranslateFn(func() (x, y float32) {
p := offset.Get()
return p.X, p.Y
})),
OnDragStart(func(tela.DragEvent) { start.Current = offset.Latest() }),
OnDrag(func(e tela.DragEvent) {
offset.Set(Offset{start.Current.X + e.OffsetX, start.Current.Y + e.OffsetY})
}),
OnDragEnd(func(e tela.DragEvent) {
if e.VelocityX > 800 {
// a fling to the right
}
}),
Text("Drag me"),
)
}

The examples/board kanban board uses this to move cards between columns.

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

func Item(name string) tela.Node {
return Div(OnLongPress(func(e tela.PointerEvent) { showMenu(e.X, e.Y) }), Text(name))
}
func showMenu(x, y float32) {}

In a test, hold the button with telatest.Delay or advance the manual clock; see Testing.

OnWheel observes a wheel notch or a touchpad scroll (DOM signs: positive DeltaY scrolls down; a notch is 53 pixels, and Shift turns a vertical notch horizontal). Unless a handler calls PreventDefault(), the nearest scrolling element scrolls:

func Canvas(zoom *tela.Value[float32]) tela.Node {
return Div(OnWheel(func(e tela.WheelEvent) {
if e.Ctrl { // Ctrl+wheel zooms instead of scrolling the page
e.PreventDefault()
zoom.Update(func(z float32) float32 { return z * (1 - e.DeltaY/500) })
}
}))
}

OnScroll is covered in Scrolling and VirtualList.