Focus Reference
Overview
Focus determines which element receives keyboard input. go-tui provides three layers of focus management:
- Element-level -- individual elements marked as focusable, with
Focus()andBlur()methods - focusManager -- internal tracking used by
Appto cycle through focusable elements withFocusNext()andFocusPrev() - FocusGroup -- a state-driven helper for Tab/Shift+Tab cycling between logical sections of your UI
- Modal focus trapping -- when a modal is open with
trapFocus, Tab/Shift+Tab only cycle through elements inside the modal overlay, and unhandled keys don't reach parent handlers
Most applications use element-level focus through .gsx attributes and App-level navigation. FocusGroup is useful when you need to toggle between distinct panels or sections rather than individual elements. Modals with trapFocus enabled restrict both Tab navigation and keyboard input to the overlay content. With trapFocus disabled, keys pass through to parents normally.
Focusable Interface
type Focusable interface {
IsFocusable() bool
HandleEvent(event Event) bool
Focus()
Blur()
}
Focusable is the contract for anything that can receive keyboard focus. Element satisfies this interface directly, so you rarely need to implement it yourself.
IsFocusable
IsFocusable() bool
Returns whether this element can currently receive focus. Returns the value set by WithFocusable or SetFocusable (defaults to false).
HandleEvent
HandleEvent(event Event) bool
Processes a keyboard or mouse event. Returns true if the event was consumed (preventing further propagation), false otherwise. For scrollable elements, Element.HandleEvent automatically handles arrow keys, Page Up/Down, Home/End, and mouse wheel events.
Focus
Focus()
Called when the element gains focus. Implementations typically update visual state, like changing a border color or adding a highlight. Element.Focus sets an internal focused flag and fires the onFocus callback if one was registered.
Blur
Blur()
Called when the element loses focus. Reverses whatever visual change Focus applied. Element.Blur clears the internal focused flag and fires the onBlur callback if one was registered.
Element Focus API
Element implements Focusable and adds several focus-related methods and options.
Option Functions
WithFocusable
func WithFocusable(focusable bool) Option
Sets whether the element can receive focus. Elements are not focusable by default.
In .gsx:
<div focusable={true}>
<span>I can receive focus</span>
</div>
WithOnFocus
func WithOnFocus(fn func(*Element)) Option
Sets a callback that fires when the element gains focus. The callback receives the element itself. Implicitly sets focusable = true.
In .gsx:
<div onFocus={s.handleFocus}>
<span>Focus me</span>
</div>
WithOnBlur
func WithOnBlur(fn func(*Element)) Option
Sets a callback that fires when the element loses focus. The callback receives the element itself. Implicitly sets focusable = true.
In .gsx:
<div onBlur={s.handleBlur}>
<span>Blur me</span>
</div>
Methods
IsFocused
func (e *Element) IsFocused() bool
Returns whether this element currently has focus. Use in render methods to apply conditional styling.
// Helper function for focus-dependent styling
func panelBorderStyle(ref *tui.Ref) tui.Style {
if ref.El() != nil && ref.El().IsFocused() {
return tui.NewStyle().Foreground(tui.ANSIColor(tui.Cyan))
}
return tui.NewStyle().Foreground(tui.ANSIColor(tui.White))
}
templ (s *myComp) Render() {
<div ref={s.panel} focusable={true} borderStyle={panelBorderStyle(s.panel)} class="border-rounded p-1">
<span>Panel content</span>
</div>
}
SetFocusable
func (e *Element) SetFocusable(focusable bool)
Changes whether the element can receive focus at runtime.
SetOnFocus
func (e *Element) SetOnFocus(fn func(*Element))
Sets the focus callback at runtime. Implicitly sets focusable = true.
SetOnBlur
func (e *Element) SetOnBlur(fn func(*Element))
Sets the blur callback at runtime. Implicitly sets focusable = true.
ContainsPoint
func (e *Element) ContainsPoint(x, y int) bool
Returns true if the point (x, y) falls within the element's layout bounds. Useful for mouse-based hit testing in HandleMouse implementations.
Focus Tree Discovery
The App uses these methods internally to find and register focusable elements. You normally don't need to call them directly.
WalkFocusables
func (e *Element) WalkFocusables(fn func(Focusable))
Walks the element tree depth-first, calling fn for each focusable element. Skips hidden elements and their subtrees. The App calls this after each render to discover new focusable elements.
SetOnFocusableAdded
func (e *Element) SetOnFocusableAdded(fn func(Focusable))
Sets a callback that fires when a focusable descendant is added to the tree. The App uses this to auto-register focusable elements as they appear.
App Focus Methods
The App delegates focus navigation to its internal focus manager through three methods.
FocusNext
func (a *App) FocusNext()
Moves focus to the next focusable element in document order (depth-first traversal of the element tree). Wraps around to the first element when reaching the end.
func (s *myApp) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyTab, func(ke tui.KeyEvent) {
ke.App().FocusNext()
}),
}
}
FocusPrev
func (a *App) FocusPrev()
Moves focus to the previous focusable element. Wraps around to the last element when reaching the beginning.
tui.On(tui.KeyTab, func(ke tui.KeyEvent) {
if ke.Mod.Has(tui.ModShift) {
ke.App().FocusPrev()
} else {
ke.App().FocusNext()
}
})
Focused
func (a *App) Focused() Focusable
Returns the currently focused element, or nil if nothing has focus.
if focused := app.Focused(); focused != nil {
// Something has focus
}
FocusGroup
FocusGroup manages Tab/Shift+Tab cycling between logical sections of a UI. Each section is represented by a *State[bool] that indicates whether that section is active. FocusGroup enforces mutual exclusion: exactly one member is active at a time.
FocusGroup implements KeyListener (it has a KeyMap() method) but is not a Component. It participates in the key dispatch system without rendering anything.
NewFocusGroup
func NewFocusGroup(members ...*State[bool]) (*FocusGroup, error)
Creates a FocusGroup managing the given members. The constructor always activates the first member (true) and deactivates the rest (false), regardless of their initial values. Returns an error if fewer than 2 members are provided.
panelA := tui.NewState(false)
panelB := tui.NewState(false)
panelC := tui.NewState(false)
fg, err := tui.NewFocusGroup(panelA, panelB, panelC)
// panelA is now true, panelB and panelC are false
if err != nil {
// handle error
}
MustNewFocusGroup
func MustNewFocusGroup(members ...*State[bool]) *FocusGroup
Same as NewFocusGroup but panics on error. Use when the member count is known at compile time.
fg := tui.MustNewFocusGroup(panelA, panelB, panelC)
Next
func (fg *FocusGroup) Next()
Deactivates the current member (sets its state to false) and activates the next one (sets to true), wrapping from the last member back to the first.
Prev
func (fg *FocusGroup) Prev()
Deactivates the current member and activates the previous one, wrapping from the first member to the last.
Current
func (fg *FocusGroup) Current() int
Returns the zero-based index of the currently active member.
KeyMap
func (fg *FocusGroup) KeyMap() KeyMap
Returns two key bindings:
- Tab (no modifiers) calls
Next() - Shift+Tab calls
Prev()
Both bindings use Stop: false, so the events continue propagating after the focus group handles them.
func (s *myApp) KeyMap() tui.KeyMap {
return tui.KeyMap{
// FocusGroup bindings first
s.focusGroup.KeyMap()[0],
s.focusGroup.KeyMap()[1],
// Then app-level bindings
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) {
ke.App().Stop()
}),
}
}
Or spread the entire KeyMap:
func (s *myApp) KeyMap() tui.KeyMap {
km := s.focusGroup.KeyMap()
km = append(km,
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) {
ke.App().Stop()
}),
)
return km
}
Complete Example
A three-panel layout with FocusGroup-driven Tab cycling. Each panel highlights its border when active.
package main
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
type panels struct {
panelA *tui.State[bool]
panelB *tui.State[bool]
panelC *tui.State[bool]
focusGroup *tui.FocusGroup
}
func Panels() *panels {
a := tui.NewState(false)
b := tui.NewState(false)
c := tui.NewState(false)
return &panels{
panelA: a,
panelB: b,
panelC: c,
focusGroup: tui.MustNewFocusGroup(a, b, c),
}
}
func (p *panels) KeyMap() tui.KeyMap {
km := p.focusGroup.KeyMap()
km = append(km,
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) {
ke.App().Stop()
}),
)
return km
}
func panelStyle(active bool) string {
if active {
return "text-cyan font-bold"
}
return "font-dim"
}
func panelBorder(active bool) tui.Style {
if active {
return tui.NewStyle().Foreground(tui.ANSIColor(tui.Cyan))
}
return tui.NewStyle().Foreground(tui.ANSIColor(tui.White))
}
templ (p *panels) Render() {
<div class="flex-row gap-1 p-1" height={20}>
<Panel title="Panel A" active={p.panelA.Get()} />
<Panel title="Panel B" active={p.panelB.Get()} />
<Panel title="Panel C" active={p.panelC.Get()} />
</div>
}
templ Panel(title string, active bool) {
<div class="flex-col flex-1 border-rounded p-1" borderStyle={panelBorder(active)}>
<span class={panelStyle(active)}>{title}</span>
if active {
<span class="text-cyan">{fmt.Sprintf("%s is active", title)}</span>
} else {
<span class="text-dim">Press Tab to focus</span>
}
</div>
}
See Also
- Events Reference -- key and mouse event types
- State Reference --
State[T]used by FocusGroup members - App Reference --
FocusNext,FocusPrev,Focusedmethods - Focus Guide -- practical patterns for focus management