Skip to content
Keyboard and focus

Keyboard and focus

Many people use an app from the keyboard alone, and screen-reader users mostly do. Every control they need should be reachable with Tab and operable with keys.

The focus order

On desktop, Tab moves focus to the next widget that accepts it, and Shift+Tab to the previous one. At either end, focus wraps around.

// The focus order is the order in the tree: name, email, notes, then the
// button. The disabled check is skipped. Tab stops at notes: a multi-line
// entry keeps Tab and Shift+Tab, so the keyboard cannot move focus past it.
name := widget.NewEntry()
email := widget.NewEntry()
notes := widget.NewMultiLineEntry() // takes Tab itself, to insert a tab
later := widget.NewCheck("Remind me later", nil)
later.Disable()
send := widget.NewButton("Send", func() {})

w.SetContent(container.NewVBox(name, email, notes, later, send))
  • The order is the order of the widget tree: the order of objects in their containers, depth first. To change the order, change the layout.
  • Disabled widgets are skipped.
  • Under a right-to-left layout, Bellui reorders focus to follow the screen: rows from top to bottom, and each row from right to left. A single column keeps its order.
  • Escape closes the topmost pop-up, menu or dialog, unless the focused widget uses Escape itself.

Buttons are activated with Space when they have focus.

Set the first focused widget

For a window whose purpose is to type something, focus the first field when you build it, so the user can start typing straight away:

w.Canvas().Focus(name) // start typing straight away

The canvas also has FocusNext, FocusPrevious, Unfocus and Focused.

Widgets that keep Tab

A widget that implements event.Tabbable and returns true from AcceptsTab receives Tab itself, instead of moving focus. A multi-line entry does this so that Tab inserts a tab character. It also keeps Shift+Tab, and Bellui gives it no other key that moves focus out, so a keyboard user who reaches one cannot Tab past it.

If you write such a widget, give users another key that leaves it, and move focus on yourself with the canvas’s FocusNext. A widget that keeps Tab with no way out traps keyboard users.

Make a custom widget focusable

A widget receives focus and keys by implementing event.Focusable:

var _ event.Focusable = (*Rating)(nil)

func (r *Rating) FocusGained()   { r.focused = true; r.update() }
func (r *Rating) FocusLost()     { r.focused = false; r.update() }
func (r *Rating) TypedRune(rune) {}

// TypedKey gives keyboard users the same control as the pointer.
func (r *Rating) TypedKey(ev *event.KeyEvent) {
	switch ev.Name {
	case event.KeyRight, event.KeyUp:
		r.set(r.Value + 1)
	case event.KeyLeft, event.KeyDown:
		r.set(r.Value - 1)
	case event.KeyHome:
		r.set(0)
	}
}
  • FocusGained and FocusLost tell the widget when it gains and loses focus. Show it: users must be able to see where focus is. The rating widget puts brackets around its stars.
  • TypedKey receives keys such as the arrows, Home and Space.
  • TypedRune receives typed characters.

Give keyboard users the same control a pointer user has. The rating widget uses the arrow keys for what a tap does.

A focusable widget that a key activates, as Space activates a button, can also implement event.Activatable. ActivatedByKey reports which keys activate it. Bellui then reports that key press as an activation, the same signal a tap sends. The widget still handles the key itself in TypedKey.

The full program:

// Keyboard focus: where it starts, and the order Tab follows.
package main

import (
	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/widget"
)

func main() {
	a := app.New()
	w := a.NewWindow("Keyboard")

	// The focus order is the order in the tree: name, email, notes, then the
	// button. The disabled check is skipped. Tab stops at notes: a multi-line
	// entry keeps Tab and Shift+Tab, so the keyboard cannot move focus past it.
	name := widget.NewEntry()
	email := widget.NewEntry()
	notes := widget.NewMultiLineEntry() // takes Tab itself, to insert a tab
	later := widget.NewCheck("Remind me later", nil)
	later.Disable()
	send := widget.NewButton("Send", func() {})

	w.SetContent(container.NewVBox(name, email, notes, later, send))

	w.Canvas().Focus(name) // start typing straight away

	bellui.ShowAndRun(w)
}