/ Game Dev With AI / Godot UI Tutorial: Build a Mouse and Gamepad Menu
Game Dev With AI 9 min read

Godot UI Tutorial: Build a Mouse and Gamepad Menu

Follow a practical Godot UI tutorial to build a responsive menu with containers, anchors, keyboard and controller focus, and a working pause state.

Modular game menu panels connected by orange controller focus paths

A useful Godot UI tutorial should leave you with a menu that works after the mouse is unplugged and the window changes size. Build the layout with Control nodes and containers, set an initial focused button in code, define intentional focus neighbors, and test the same scene with a mouse, keyboard, and controller.

This walkthrough builds one small main menu and turns the same structure into a pause menu. The finished scene has Start, Options, and Quit buttons, keeps the stack centered when the window resizes, and gives keyboard or gamepad users an obvious place to begin.

Godot's official UI documentation separates controls into content nodes, such as buttons and labels, and layout nodes, such as box, margin, and scroll containers. That distinction is the whole foundation here: content says what the player can use, while containers decide where it goes.

Build the Menu Scene Tree

Create a new scene with a Control root named MainMenu. Set its layout preset to Full Rect so it follows the viewport.

Add this node tree:

MainMenu (Control)
└── Background (ColorRect)
    └── MarginContainer
        └── CenterContainer
            └── MenuStack (VBoxContainer)
                ├── GameTitle (Label)
                ├── StartButton (Button)
                ├── OptionsButton (Button)
                └── QuitButton (Button)

Set Background, MarginContainer, and CenterContainer to Full Rect. Give the margin container a modest inset on all four sides. The exact value is a visual choice, but the margin matters because a centered menu should not touch the edge on a narrow window.

The hierarchy has a job for every layer:

  • ColorRect provides a predictable background.
  • MarginContainer protects an edge gutter.
  • CenterContainer positions one child in the available space.
  • VBoxContainer stacks the label and buttons vertically.
  • the label and buttons contain the actual interface.

Do not hand-position each button with separate pixel offsets. That looks correct at the design resolution and drifts when text, font size, localization, or window dimensions change. A container owns the position of its children, so let it do the layout work.

If you need a refresher on scenes and nodes before continuing, start with making your first game in Godot or the longer 2D game guide.

Configure the Content Before Styling It

Set the label text to the working game title. Set the three button labels to Start, Options, and Quit. Give MenuStack a theme override for vertical separation so the controls do not touch.

Now make each button easy to identify from code. In the Inspector, enable Unique Name in Owner for StartButton, OptionsButton, and QuitButton. Godot then lets the script reference them with the %Name shorthand instead of a fragile chain such as $Background/MarginContainer/CenterContainer/MenuStack/StartButton.

The scene is already usable with a mouse. That is the point where many menu tutorials stop, even though the keyboard has no selected control when the scene first opens.

Attach a script to MainMenu:

extends Control

@onready var start_button: Button = %StartButton
@onready var options_button: Button = %OptionsButton
@onready var quit_button: Button = %QuitButton

func _ready() -> void:
    start_button.grab_focus.call_deferred()
    start_button.pressed.connect(_on_start_pressed)
    options_button.pressed.connect(_on_options_pressed)
    quit_button.pressed.connect(_on_quit_pressed)

func _on_start_pressed() -> void:
    print("Start the game")

func _on_options_pressed() -> void:
    print("Open options")

func _on_quit_pressed() -> void:
    get_tree().quit()

Godot's current keyboard and controller navigation guide explicitly requires a control to receive focus in code when the scene starts. Its example also defers grab_focus(). Deferring the call lets the scene finish its setup before focus is assigned.

The print() calls are honest temporary behavior. Replace them only when the destination scenes exist. A button that prints a clear signal is easier to verify than one wired early to a half-built scene transition.

Define a Focus Path Instead of Trusting a Guess

Godot can guess where focus should move when no neighbor is configured. The documentation warns that automatic guessing can produce unintended navigation in complex interfaces. This vertical menu is simple, but setting neighbors now gives you a pattern that survives later additions.

Select StartButton. Under Control > Focus, set its bottom neighbor to OptionsButton and its top neighbor to QuitButton. Then set:

Button Focus Up Focus Down
Start Quit Options
Options Start Quit
Quit Options Start

The wraparound is optional, but it makes a three-item menu pleasant to navigate. Set Next and Previous too if you want Tab and Shift+Tab to follow the same order.

Keep the built-in actions ui_up, ui_down, ui_left, ui_right, and ui_accept for interface navigation. Godot warns against reusing those actions for gameplay because the interface consumes them for focus. Create separate actions such as move_up and interact for the character.

Run the scene without touching the mouse. The Start button should have a visible focused state. Press Down three times, then Up three times. Press Enter or the controller's accept button on each item. If you cannot see which item is active, the navigation may technically work but the menu is not readable.

Make the Focus State Visible

A focused button needs more than a tiny color change that disappears into the art. Use a theme so every button shares the same normal, hover, pressed, disabled, and focus treatment.

Create a Theme resource on the root or menu stack. You can change font sizes, colors, and style boxes in the theme editor. Keep the visual language simple while the behavior is unfinished:

  • normal: quiet surface and readable text;
  • hover: clear pointer response;
  • focus: strong outline or high-contrast surface;
  • pressed: immediate depth or color change;
  • disabled: visibly unavailable without becoming illegible.

Do not remove the focus style because it looks less cinematic. It is the only persistent signal for someone navigating without a pointer. A focus ring is interface state, not decoration.

Also check that the label cannot receive focus. Labels default to a non-focusable mode, while buttons usually accept mouse, keyboard, and controller focus. If a decorative control enters the focus path, set Focus Mode to None.

Turn the Structure Into a Pause Menu

Duplicate the scene as PauseMenu.tscn. Change the button labels to Resume, Options, and Quit to Main Menu. Rename the unique nodes to match.

The pause controller must receive the pause action both before and after the game tree pauses. Set the pause menu root's Process > Mode to Always in the Inspector. Gameplay nodes can keep their normal pausable behavior. Start with the menu hidden, then expose two functions:

extends Control

@onready var resume_button: Button = %ResumeButton

func open_menu() -> void:
    visible = true
    get_tree().paused = true
    resume_button.grab_focus.call_deferred()

func close_menu() -> void:
    visible = false
    get_tree().paused = false

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("pause"):
        if visible:
            close_menu()
        else:
            open_menu()
        get_viewport().set_input_as_handled()

Add a pause action in Project Settings > Input Map and bind it to Escape plus the controller button you intend to support. Connect the Resume button to close_menu().

Notice that focus is reassigned every time the menu opens. Godot notes that a hidden control can lose focus. Assuming yesterday's focused button will still be active is how pause menus become mouse-only after the second opening.

Decide separately what Quit should mean. Quitting the application, returning to a title scene, and abandoning unsaved progress are different product decisions. The sample main menu uses get_tree().quit() because it is unambiguous there. A pause menu should not silently apply the same behavior.

Run a Five-Part Menu Test

Treat this as a small acceptance test, not a visual glance.

1. Mouse-Only Test

Open every button with the pointer. Verify hover and pressed states. Move the pointer away after clicking and confirm that keyboard navigation still has a visible focused control.

2. Keyboard-Only Test

Restart the scene and never touch the mouse. Move forward and backward through every button. Activate each one. Open and close the pause menu twice.

3. Controller-Only Test

Disconnect the mouse if practical. Use the D-pad, then an analog direction if your input mapping supports it. Confirm that the accept and back actions do what their labels imply.

4. Resize Test

Drag the window through wide, tall, and small dimensions. The stack should remain centered with a safe margin. No label should overlap a button, and the focused item must remain visible.

5. Pause-State Test

Open the pause menu while gameplay is moving. Confirm that gameplay stops, the menu still responds, Resume restores gameplay, and reopening the menu restores initial focus. Also confirm that input used to close the menu does not leak into the game as an action.

Record failures as behavior, not mood. “Focus disappears after Options closes” is actionable. “Controller feels weird” is not yet specific enough.

Common Godot UI Problems

The Buttons Do Not Move When the Window Resizes

Check the layout preset on every wrapper. A Full Rect root does not automatically make a nested background or center container follow it. Also check whether you manually changed a child's offsets after placing it inside a container.

The Controller Does Nothing on First Load

Call grab_focus.call_deferred() on the intended first button. Then verify that the button's focus mode accepts keyboard and controller focus.

Focus Jumps to a Surprising Control

Set explicit focus neighbors. Automatic navigation is a fallback, not a designed path.

The Pause Menu Appears but Cannot Be Used

Set the pause controller root to Always so it can open before the tree pauses and close afterward. Then make sure the input handler and button signals live under that root or another node with equivalent processing behavior.

The Menu Works With a Mouse but Looks Dead on Gamepad

Strengthen the theme's focus state. Hover and focus are different states and need separate visual treatment.

The Small Definition of Done

This menu is done when a fresh scene load selects Start, every item is reachable in both directions, resizing preserves the layout, and the pause menu can open, close, and regain focus twice without a mouse.

That is a better milestone than “UI polished.” Once the behavior passes, you can replace temporary labels, add audio, animate transitions, and expand the Options screen without rebuilding the navigation foundation.