Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Game Development with Godot & GDScript  Module 11 – UI, menus, accessibility and localization

Focus navigation and accessibility

Make Godot 4.7 menus work with keyboard and controller focus, fix the focus neighbours Godot guesses, name controls for screen readers and check colours.

  • Intermediate
  • 20 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Nothing works with a controller until one control has the focus · Godot guesses each focus neighbour from where controls sit · Screen readers read names, not icons. With a pass: 3 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Build menus that keyboard and controller players can use from the moment they open
  • Set focus neighbours where Godot guesses wrong, and keep the focus when a control hides
  • Name icon-only controls and live text for screen readers with the accessibility properties
  • Check the contrast and colour-blind safety of a HUD palette in code

Before you start

Two presses of down on a controller moved nothing in this page’s first run, because no control of the pause menu had the focus. Menus only ever tried with a mouse fail this way, as do icon buttons a screen reader cannot name and health bars that turn from green to red for players who cannot tell the two apart.

This page gives a menu its first focus, walks focus navigation with the ui_* actions, fixes the neighbours Godot guesses wrong and audits a HUD for controls without a name. Every run is headless Godot 4.7.2, with key presses sent from code.

Nothing works with a controller until one control has the focus

The arrow keys, Tab and a controller’s d-pad do not move a cursor in a Godot menu; they move the focus: The one control that receives keyboard and controller input, such as the highlighted button of a menu. The ui_* actions move it to a neighbour, grab_focus() gives it to a control, and a control whose focus_mode is FOCUS_NONE never gets it., through the built-in actions ui_up, ui_down, ui_focus_next and the others. focus_walk.gd opens a pause menu twice, first with its _ready() removed and then as written, and presses those actions:

pause_menu.gd: Resume takes the focus GD · focus/pause_menu.gd
extends CenterContainer
## A pause menu that a keyboard or a controller can use at once: Resume takes the focus when the menu
## opens, after the containers have placed it.


func _ready() -> void:
	$Column/Resume.grab_focus.call_deferred()
focus_walk.gd: the menu walked with the ui_* actions GD · focus/focus_walk.gd
extends SceneTree
## A pause menu with and without a focused control, walked with the ui_* actions that the arrow keys,
## Tab and a controller's d-pad send.


func press(action: StringName) -> void:
	for pressed in [true, false]:
		var event := InputEventAction.new()
		event.action = action
		event.pressed = pressed
		Input.parse_input_event(event)
		await process_frame


func focused() -> String:
	var owner := root.gui_get_focus_owner()
	return owner.name if owner else "nothing"


func walk(actions: Array[StringName]) -> String:
	var steps: PackedStringArray = [focused()]
	for action in actions:
		await press(action)
		steps.append(focused())
	return " -> ".join(steps)


func _initialize() -> void:
	await process_frame
	root.size = Vector2i(1920, 1080)
	var menu: Control = load("res://pause_menu.tscn").instantiate()
	menu.set_script(null)
	root.add_child(menu)
	await process_frame
	print("Without grab_focus(): ui_down, ui_down: %s" % await walk([&"ui_down", &"ui_down"]))
	menu.free()

	menu = load("res://pause_menu.tscn").instantiate()
	root.add_child(menu)
	await process_frame
	print("With grab_focus() in _ready():")
	print("  ui_down x3: %s" % await walk([&"ui_down", &"ui_down", &"ui_down"]))
	print("  ui_up x3: %s" % await walk([&"ui_up", &"ui_up", &"ui_up"]))
	print("  ui_focus_prev, ui_focus_next: %s" % await walk([&"ui_focus_prev", &"ui_focus_next"]))
	var title: Label = menu.get_node("Column/Title")
	var resume: Button = menu.get_node("Column/Resume")
	print("focus_mode: Title %d (FOCUS_NONE), Resume %d (FOCUS_ALL)" % [title.focus_mode, resume.focus_mode])
	resume.grab_focus(true)
	print("grab_focus(true), as after a mouse click: has_focus() %s, has_focus(true) %s" % [resume.has_focus(), resume.has_focus(true)])
	menu.free()
	quit()
A pause menu walked with and without a focused control, Godot 4.7.2 Python · walk_session.py
from session import files, run

# A pause menu walked with the ui_* actions, without and with a control that has the focus.
files("project.godot", "focus/pause_menu.tscn=pause_menu.tscn", "focus/pause_menu.gd=pause_menu.gd", "focus/focus_walk.gd=focus_walk.gd")
run("""
godot --headless --no-header --script focus_walk.gd
""")

Output

$ godot --headless --no-header --script focus_walk.gd
Without grab_focus(): ui_down, ui_down: nothing -> nothing -> nothing
With grab_focus() in _ready():
  ui_down x3: Resume -> Options -> Quit -> Quit
  ui_up x3: Quit -> Options -> Resume -> Resume
  ui_focus_prev, ui_focus_next: Resume -> Quit -> Resume
focus_mode: Title 0 (FOCUS_NONE), Resume 2 (FOCUS_ALL)
grab_focus(true), as after a mouse click: has_focus() true, has_focus(true) false

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 walk_session.py

Without a focused control, two presses of ui_down did nothing. The navigation page says some control must be focused by code when the scene starts, and its example calls grab_focus.call_deferred() in _ready(), as pause_menu.gd does. Follow the run:

Interactive · stage by stage Where the focus goes in the pause menu Step through the run. Each step is one press and the control that has the focus after it.
Where the focus goes in the pause menu, step 5 of 5. Showing Title, a Label, Resume (FOCUS_ALL), Options, Quit (has the focus). In focus: Resume, Quit.ui_focus_prevTitle, a LabelFOCUS_NONE: never focusedResumeFOCUS_ALLOptionsFOCUS_ALLQuithas the focusui_focus_prev

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. No focus. Without a grab_focus() call nothing has the focus, and two presses of ui_down moved nothing: a controller player sees a menu that ignores them.
  2. Resume takes the focus. pause_menu.gd calls $Column/Resume.grab_focus.call_deferred() in _ready(), so Resume has the focus once the containers have placed it.
  3. Down the column. ui_down moved the focus to Options, then to Quit. Godot skipped Title because a Label cannot take the focus.
  4. The end of the column. One more ui_down left the focus on Quit. Arrow keys and the d-pad stop at the last control unless you set a neighbour that wraps.
  5. Tab goes round. ui_focus_prev, Shift and Tab, from Resume went round to Quit, and ui_focus_next came back: the Tab order wraps, the arrows do not.

focus_mode decides which controls can be focused: Label is FOCUS_NONE, so the walk skipped Title, and Button is FOCUS_ALL. Since 4.6 a mouse click no longer shows the focus outline: grab_focus(true) gave Resume hidden focus, which has_focus(true) reports as false.

Testing a menu with the mouse only

A menu clicked through with a mouse works with nothing focused. Press ui_down first, as the run did: a menu that answers nothing lacks its grab_focus() call.

Checkpoint

Check: The first focus and the ui_* actions

  1. Question 1 of 2 A pause menu opens and nothing calls grab_focus(). A controller player presses down on the d-pad twice. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing: with no control focused, ui_down has nothing to move from

    Give the first control the focus in _ready(), with grab_focus.call_deferred(), as the navigation page shows.

  2. Question 2 of 2 The menu column is Title (a Label), Resume, Options and Quit. The focus is on Resume, and the player presses ui_up. Where is the focus now?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Still on Resume

    focus_mode decides which controls the walk can stop on; arrows stop at the ends.

Godot guesses each focus neighbour from where controls sit

The Control reference gives every control four focus neighbour: The control that the focus moves to when the player presses up, down, left or right, set by focus_neighbor_top, _bottom, _left and _right. When one is empty, Godot guesses it from where the controls are., focus_neighbor_left, _top, _right and _bottom, plus focus_next and focus_previous for Tab. When they are empty, the navigation page says Godot “will try to guess the next control automatically”. neighbours.gd prints the guesses for an inventory of 12 slots in a grid with three buttons beside it, fixes three, and then hides the focused slot:

neighbours.gd: guessed neighbours, three set by hand, a hidden slot GD · focus/neighbours.gd
extends SceneTree
## The neighbours Godot guesses for an inventory of 12 slots and a column of three buttons, three of
## them fixed by hand, and the focus lost when the focused slot is hidden.

const SIDES := {SIDE_LEFT: "left", SIDE_TOP: "up", SIDE_RIGHT: "right", SIDE_BOTTOM: "down"}


func press(action: StringName) -> void:
	for pressed in [true, false]:
		var event := InputEventAction.new()
		event.action = action
		event.pressed = pressed
		Input.parse_input_event(event)
		await process_frame


func guess(control: Control, side: Side) -> String:
	var next := control.find_valid_focus_neighbor(side)
	return next.name if next else "nothing"


func report(inventory: Control, paths: Array[String]) -> void:
	for path in paths:
		var control: Control = inventory.get_node(path)
		var parts: PackedStringArray = []
		for side: Side in SIDES:
			parts.append("%s %s" % [SIDES[side], guess(control, side)])
		print("  %-6s %s" % [control.name, ", ".join(parts)])


func _initialize() -> void:
	await process_frame
	root.size = Vector2i(1920, 1080)
	var inventory: Control = load("res://inventory.tscn").instantiate()
	root.add_child(inventory)
	await process_frame
	var watched: Array[String] = ["Slots/Slot4", "Slots/Slot8", "Slots/Slot12", "Actions/Equip", "Actions/Drop", "Actions/Close"]
	print("Guessed by Godot:")
	report(inventory, watched)

	var slot8: Button = inventory.get_node("Slots/Slot8")
	var drop: Button = inventory.get_node("Actions/Drop")
	var close: Button = inventory.get_node("Actions/Close")
	print("Middles, from the top: Slot8 %d, Drop %d, Close %d" % [slot8.get_global_rect().get_center().y,
			drop.get_global_rect().get_center().y, close.get_global_rect().get_center().y])
	slot8.focus_neighbor_right = slot8.get_path_to(drop)
	drop.focus_neighbor_left = drop.get_path_to(slot8)
	close.focus_neighbor_bottom = close.get_path_to(close)
	print("With three neighbours set by hand:")
	report(inventory, ["Slots/Slot8", "Actions/Drop", "Actions/Close"])

	var slot5: Button = inventory.get_node("Slots/Slot5")
	slot5.grab_focus()
	await process_frame
	slot5.hide()
	await process_frame
	var owner := root.gui_get_focus_owner()
	print("Slot5 had the focus and was hidden: the focus is on %s" % (owner.name if owner else "nothing"))
	await press(&"ui_right")
	owner = root.gui_get_focus_owner()
	print("  ui_right then reaches %s" % (owner.name if owner else "nothing"))
	quit()
The focus neighbours of an inventory, Godot 4.7.2 Python · neighbours_session.py
from session import files, run

# Guessed focus neighbours of an inventory, three set by hand, and the focus lost when a slot is hidden.
files("project.godot", "focus/inventory.tscn=inventory.tscn", "focus/neighbours.gd=neighbours.gd")
run("""
godot --headless --no-header --script neighbours.gd
""")

Output

$ godot --headless --no-header --script neighbours.gd
Guessed by Godot:
  Slot4  left Slot3, up nothing, right Equip, down Slot8
  Slot8  left Slot7, up Slot4, right Close, down Slot12
  Slot12 left Slot11, up Slot8, right Close, down nothing
  Equip  left Slot4, up nothing, right nothing, down Drop
  Drop   left Slot4, up Equip, right nothing, down Close
  Close  left Slot8, up Drop, right nothing, down Slot8
Middles, from the top: Slot8 200, Drop 164, Close 208
With three neighbours set by hand:
  Slot8  left Slot7, up Slot4, right Drop, down Slot12
  Drop   left Slot8, up Equip, right nothing, down Close
  Close  left Slot8, up Drop, right nothing, down Close
Slot5 had the focus and was hidden: the focus is on nothing
  ui_right then reaches nothing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 neighbours_session.py

Most guesses were right; three were not what the screen means:

Interactive · step by step Three focus neighbours that Godot guessed, and the fix Step through the guesses of the run. Arrows are where a press sends the focus.
Three focus neighbours that Godot guessed, and the fix, step 5 of 5. Showing Slot4, Slot8, Slot12, Equip, Drop, Close (down stays here). In focus: Slot8, Drop, Close.rightleftSlot4row 1 of the gridSlot8row 2 of the gridSlot12row 3 of the gridEquipDropClosedown stays here

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The guesses. No neighbour is set, so Godot works each one out from where the controls are. Most guesses were right: Slot4 leads right to Equip and Slot12 to Close.
  2. Slot8, right. From Slot8, in the middle row, right went to Close, whose middle is 8 px from Slot8's, not to Drop, 36 px away. The guess follows the geometry; the design pairs each row with the action beside it.
  3. Close, down. From Close, the last button, down went back into the grid, to Slot8. A player pressing down to stay at the bottom of the menu lands somewhere else.
  4. Drop, left. From Drop, left went up to Slot4 instead of across to Slot8, so right and then left does not bring a player back where they started.
  5. Three neighbours by hand. Setting focus_neighbor_right of Slot8 to Drop, focus_neighbor_left of Drop to Slot8, and focus_neighbor_bottom of Close to Close itself fixed all three.

Good focus navigation is predictable: right and then left comes back, and down at the bottom stays. Set the neighbours where the guess and the design differ, by NodePath, as the run did.

The last lines show the other way focus is lost: when the focused Slot5 was hidden, the focus went to nothing, and so did ui_right. The navigation page warns that a hidden node can lose the focus, and a controller then does nothing.

Key idea

Give the first control the focus in code, set neighbours where Godot’s guess and the design differ, and move the focus on before a focused control hides.

Checkpoint

Check: Focus neighbours, guessed and set

  1. Question 1 of 2 Slot8's middle is at y 200, Drop's at 164 and Close's at 208, and no neighbours are set. Where does ui_right from Slot8 go?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: To Close

    A guess follows positions; set focus_neighbor_right where the design pairs controls differently.

  2. Question 2 of 2 When an item is used up, its slot is hidden. The slot often has the focus at that moment. What should the code do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Give the focus to another control, such as the next valid one, as it hides the slot

    Whenever a focused control hides or is freed, decide where the focus goes next.

Screen readers read names, not icons

Godot 4.5 added screen reader support through AccessKit, and its release notes called the integration experimental. A screen reader: An assistive app that reads out what is on screen and which control has the focus, used by blind and low-vision players. Godot reports each control's name, role and description to it through AccessKit. announces each control by its name, so a Button with text has words to report and an icon-only button has none of its own. name_audit.gd audits the top bar of a HUD for controls a player can focus but that have no text and no accessibility_name, names them, and audits again:

name_audit.gd: an audit for controls without a name GD · names/name_audit.gd
extends SceneTree
## An accessibility audit of a HUD's top bar: every control a player can focus needs a name a screen
## reader can read, its text or its accessibility_name. The bar has two buttons with text and three
## with an icon only; the audit runs before and after the icon buttons get names.


func icon() -> Texture2D:
	return ImageTexture.create_from_image(Image.create(24, 24, false, Image.FORMAT_RGBA8))


## Every control under root that can take the focus and has no name to read.
func unnamed(root_control: Control) -> PackedStringArray:
	var found: PackedStringArray = []
	for control: Control in root_control.find_children("*", "Control", true, false):
		if control.get_focus_mode_with_override() == Control.FOCUS_NONE:
			continue
		var text: String = control.get("text") if "text" in control else ""
		if text.strip_edges().is_empty() and control.accessibility_name.strip_edges().is_empty():
			found.append(control.name)
	return found


func _initialize() -> void:
	await process_frame
	var bar := HBoxContainer.new()
	root.add_child(bar)
	for spec: Array in [["Resume", "Resume", null], ["Options", "Options", null], ["Map", "", icon()], ["Bag", "", icon()], ["Mute", "", icon()]]:
		var button := Button.new()
		button.name = spec[0]
		button.text = spec[1]
		button.icon = spec[2]
		bar.add_child(button)
	var coins := Label.new()
	coins.name = "Coins"
	coins.text = "Coins: 120"
	bar.add_child(coins)
	print("Before: %d controls the player can focus have nothing to read: %s" % [unnamed(bar).size(), ", ".join(unnamed(bar))])

	var names := {"Map": ["Map", "Opens the map of the region"], "Bag": ["Inventory", "Opens your bag: 12 slots"], "Mute": ["Mute music", "Turns the music off and on"]}
	for button_name: String in names:
		var button: Button = bar.get_node(button_name)
		button.accessibility_name = names[button_name][0]
		button.accessibility_description = names[button_name][1]
		button.tooltip_text = names[button_name][0]
	coins.accessibility_live = AccessibilityServer.LIVE_POLITE
	print("After: %d controls have nothing to read" % unnamed(bar).size())
	for control: Control in bar.get_children():
		var text: String = control.get("text") if "text" in control else ""
		print("  %-7s text \"%s\", accessibility_name \"%s\"" % [control.name, text, control.accessibility_name])
	print("Coins: accessibility_live %d, LIVE_POLITE: a screen reader reads each change at its next opportunity" % coins.accessibility_live)
	print("Project setting accessibility/general/accessibility_support: %d (0 is Auto)" % ProjectSettings.get_setting("accessibility/general/accessibility_support"))
	bar.free()
	quit()
A HUD bar audited for names, Godot 4.7.2 Python · names_session.py
from session import files, run

# An audit of a HUD's top bar for controls that a screen reader could not name, before and after naming them.
files("project.godot", "names/name_audit.gd=name_audit.gd")
run("""
godot --headless --no-header --script name_audit.gd
""")

Output

$ godot --headless --no-header --script name_audit.gd
Before: 3 controls the player can focus have nothing to read: Map, Bag, Mute
After: 0 controls have nothing to read
  Resume  text "Resume", accessibility_name ""
  Options text "Options", accessibility_name ""
  Map     text "", accessibility_name "Map"
  Bag     text "", accessibility_name "Inventory"
  Mute    text "", accessibility_name "Mute music"
  Coins   text "Coins: 120", accessibility_name ""
Coins: accessibility_live 1, LIVE_POLITE: a screen reader reads each change at its next opportunity
Project setting accessibility/general/accessibility_support: 0 (0 is Auto)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 names_session.py

The audit found three: Map, Bag and Mute, icons only. accessibility_name is “the human-readable node name that is reported to assistive apps”, and accessibility_description a longer explanation. The coin label got accessibility_live set to LIVE_POLITE, which makes it a live region: A control whose changes a screen reader announces without the focus moving to it, set with accessibility_live: polite changes wait for the next opportunity, assertive ones are read at once.: AccessibilityServer describes polite updates as presented “at the next opportunity”, so a screen reader reads the new count without breaking off a sentence; LIVE_ASSERTIVE, for a warning that must interrupt, is presented at once.

Two more questions belong to accessibility, and they are about colour: whether text stands out from what is behind it, measured as a contrast ratio, and whether two colours stay apart for players with colour vision deficiencies. Both answers can be worked out from the colours alone, so a script can check a palette before any player sees it. The accessibility options that players look for, such as a toggle in place of a held button, are a third.

Timeline · 3 events in order Accessibility and focus in recent Godot versions Walk through the three releases that changed how Godot menus work with screen readers and keyboards.
  1. 4.5

    Screen reader support, through AccessKit experimental

    Controls report a name, a role and a description to screen readers. The release notes call the integration experimental and say it covered the standard UI nodes, the Project Manager and the inspector at first.

  2. 4.6

    Mouse focus and keyboard focus apart

    A click no longer draws the focus outline. grab_focus(true) gives focus without showing it, and has_focus(true) ignores such hidden focus.

  3. 4.7

    Landmark navigation and AccessibilityServer

    Screen readers can tell which part of a window the focus is in. Live regions now take AccessibilityServer.AccessibilityLiveMode values, such as LIVE_POLITE.

The timeline has 3 events, from 4.5 to 4.7.

Turn accessibility support on to test it

accessibility/general/accessibility_support is Auto (0) by default: updates are processed only while an assistive app runs. Inspection tools such as Accessibility Insights do not count, so test with them at Always Active (1).

Checkpoint

Check: Names and live regions for screen readers

  1. Question 1 of 2 Which control does the audit of name_audit.gd report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A Button with only an icon and an empty accessibility_name

    Every control a player can focus needs words: its text, or accessibility_name.

  2. Question 2 of 2 The coin label has accessibility_live set to LIVE_POLITE, and the count changes while the screen reader is reading a sentence. When is the new count read?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: At the next opportunity, for example when the sentence ends

    Use LIVE_POLITE for counts and status, and LIVE_ASSERTIVE only for warnings that must interrupt.

Contrast and colour-blind safety, checked in code Pro: opens with a Pro, Premium or Ultimate pass

A palette that looks fine to its designer can fail two ways: text that does not stand out, and colours that look alike to a player with colour vision deficiency. This part measures both in GDScript with the formulas of WCAG 2.2 and a published simulation, and fixes a HUD that fails them.

In this part:

  • Contrast ratios the way WCAG computes them
  • Health bar colours through protanopia and deuteranopia

2 runnable examples (GD, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Accessibility options players look for, built in a few lines Pro: opens with a Pro, Premium or Ultimate pass

Many barriers come from how a game asks to be played: a button that must be held, a camera that shakes, text that cannot grow. This part builds three options players look for and runs each, at 60 physics ticks a second.

In this part:

  • Hold or toggle, chosen by the player
  • Screen shake and text size that players choose

2 runnable examples (GD, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Lab: an inventory that a controller and a screen reader can finish Pro: opens with a Pro, Premium or Ultimate pass

An inventory is a hard test of focus navigation: a grid, a column of actions and items that disappear. This lab gives an inventory its first focus, its neighbours and its names, and keeps the focus alive when an item is used up.

In this part:

  • The inventory, its checker and a tempting version
  • Beyond the lab: focus that players can see and follow

3 runnable examples (GD, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

Here the questions keep to focus, neighbours and names, weakest idea first, each choice with its reason.

Practice that adapts to you

Practise this lesson

9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What do the arrow keys, Tab and a controller's d-pad move in a Godot menu?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: The focus, through the ui_* actions

    The built-in ui_* actions drive the focus; leave them to the menus and give gameplay actions of its own.

  2. Question 2 of 9 The focus is on Resume, the first button. The player presses Shift and Tab, ui_focus_prev. Where does the focus go?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: To Quit, the last button

    ui_focus_next and ui_focus_prev follow focus_next and focus_previous, or the tree order, and wrap.

  3. Question 3 of 9 Since Godot 4.6, a player clicks Options with the mouse, and has_focus(true) on Options returns false while has_focus() returns true. Why?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: A click gives the focus without showing it, and has_focus(true) ignores such hidden focus

    4.6 separated mouse focus from keyboard focus, so the outline shows only when keyboard or controller players need it.

  4. Question 4 of 9 Which property decides where ui_right goes from a control?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: focus_neighbor_right

    One neighbour per direction, plus focus_next and focus_previous for Tab.

  5. Question 5 of 9 From Close, the last action, ui_down jumped into the grid. How do you make it stay on Close?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Set Close's focus_neighbor_bottom to the path of Close itself

    A neighbour that points at its own control keeps the focus in place.

  6. Question 6 of 9 Why do Godot's guesses go wrong most often between a grid and a column of buttons beside it?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Their rows do not line up, so the nearest control is not always the one the design pairs

    Check the neighbours where differently sized rows meet, and set them by hand where they differ from the design.

  7. Question 7 of 9 What is a Control's accessibility_name?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: The human-readable name reported to assistive apps such as screen readers

    Set it on every control whose text does not say what it does.

  8. Question 8 of 9 A HUD has 9 controls that can take the focus: 5 buttons with text, 3 icon buttons named with accessibility_name and 1 icon button with nothing. How many does the audit report?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: 1

    The audit's rule: a focusable control needs text or an accessibility_name.

  9. Question 9 of 9 A low-health warning must interrupt whatever the screen reader is saying. Which accessibility_live value fits it?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: LIVE_ASSERTIVE

    Keep assertive for what cannot wait, or players hear interruptions all game long.

Interview questions

Warm-up: why does a Godot menu do nothing with a gamepad until code calls grab_focus(), and where does the call go?

A gamepad’s d-pad and the arrow keys send the ui_* actions, which move the focus from the control that has it to a neighbour, so with nothing focused the presses do nothing: in a Godot 4.7.2 run, two presses of ui_down on an unfocused pause menu moved nothing. The navigation page says a control must be focused by code when the scene starts. Call grab_focus.call_deferred() on the first button in the menu’s _ready(), and again whenever the menu is shown.

  1. Mid to senior

    Controller players report that after they use up the last potion in the inventory, no slot is highlighted and the d-pad does nothing until they reopen the menu. What happened, and how do you fix it for every screen of the game?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A blind player using a screen reader says the buttons of your HUD's top bar give them nothing useful to hear, though the menus with text work. The bar's buttons are icons. How do you fix it and make sure it stays fixed?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Your health bar turns from green to red as health drops, and some players say they cannot tell when they are in danger. How do you test the colours and fix the design?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The arrow keys, Tab and the d-pad move the focus through the ui_* actions, so one control must be focused by code before a keyboard or controller can use a menu.
  • Godot guesses each empty focus neighbour from where controls sit; set the neighbours where the guess and the design differ, and move the focus on before a focused control hides.
  • A screen reader reads names: give every icon-only control an accessibility_name, and make changing text a live region.

Terms in this lesson

focus
The one control that receives keyboard and controller input, such as the highlighted button of a menu. The ui_* actions move it to a neighbour, grab_focus() gives it to a control, and a control whose focus_mode is FOCUS_NONE never gets it.
focus neighbour
The control that the focus moves to when the player presses up, down, left or right, set by focus_neighbor_top, _bottom, _left and _right. When one is empty, Godot guesses it from where the controls are.
live region
A control whose changes a screen reader announces without the focus moving to it, set with accessibility_live: polite changes wait for the next opportunity, assertive ones are read at once.
screen reader
An assistive app that reads out what is on screen and which control has the focus, used by blind and low-vision players. Godot reports each control's name, role and description to it through AccessKit.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.