Skip to content

Navigation

Navigation routes directional input, confirm, and cancel to widgets that hold focus, without depending on the mouse. A focused button responds to confirm the same way a hovered button responds to a click. The same UI becomes usable with the keyboard, a gamepad, or any other source the bindings drive.

This page explains the model. For the procedure that applies it, refer to Build a navigable menu. For the configurable inputs that trigger navigation actions, refer to Input bindings.

One active block, a set of focusable elements

Navigation lives inside a navigation block. A block is a scope that contains navigation elements. At any moment one block is active, and one element inside the active block is focused. Directional input moves focus between elements in the active block. Confirm activates the focused element. Cancel steps back out of a block that was entered by confirming an owning element.

A block is declared with BeginNavigation / EndNavigation:

ui.BeginNavigation("main_menu", active: true, initialFocus: "new_game");
{
    using (ui.Auto().Pad(20).PushSeriesVertical(32, 5))
    {
        if (MenuItem(ui, "new_game",  "New game"))  StartNewGame();
        ui.Next();
        if (MenuItem(ui, "load_game", "Load game")) LoadGame();
        ui.Next();
        if (MenuItem(ui, "exit",      "Exit"))      Quit();
    }
}
ui.EndNavigation();

MenuItem calls ui.NavigationElement(id) to register the row as a focusable element, draws a focused visual when ui.IsFocused(id) returns true, and returns ui.Button(id). Button reads Bindings.Navigation.Confirm(), so the focused row activates on the frame the confirm input fires.

How focus moves

A block stores the layout-space centre of every element registered inside it this frame. When the active block sees a directional input from Bindings.Navigation (up, down, left, or right), it considers every other element in the block as a candidate. For each one it computes the normalised direction from the focused element's centre and takes the dot product against the input direction. Candidates whose dot product falls below 0.5 — outside a ninety-degree cone around the input direction — are discarded. Among the survivors, the candidate with the highest score wins focus, where the score is the dot product minus a small distance penalty, so a closer element edges out a more perfectly-aligned but distant one.

The 0.5 cone keeps a directly-down neighbour winning a down press over an element off to the side, while still letting a slightly diagonal layout resolve. An element sitting outside the cone for one direction stays reachable through a different directional press.

There are no edges and no manual adjacency declarations: every element in the block is a candidate for every press. The element geometry rebuilds from each frame's NavigationElement calls, so reordering, removing, or inserting an element changes which candidate wins automatically on the next frame.

Confirm and cancel

Both flow through Bindings.Navigation:

  • Confirm: Button reads Bindings.Navigation.Confirm(). The focused button activates on the same frame the configured confirm input fires.
  • Cancel: EndNavigation reads Bindings.Navigation.Cancel() when the block is the active one. The built-in handler pops only a block that was entered by confirming an owning element — a NavigationElement whose id matches the inner block's id (refer to Block lifecycle and history) — and returns focus to that element. A block entered any other way, such as a plain PushNavigation, ignores the built-in cancel; the app reads Bindings.Navigation.Cancel() itself and calls PopNavigation to drive the back step.

For the inputs that map to confirm and cancel by default, and how to change them, refer to Input bindings.

Block lifecycle and history

Navigation blocks form a stack. Calling PushNavigation(id) deactivates the current active block, saves its focused element, and activates the target block on the next frame. Calling PopNavigation restores the previous block and the focus it had when it was last active.

A typical pattern is a settings submenu that pushes itself when the player chooses it. To pop it back on cancel, read Bindings.Navigation.Cancel() inside the submenu and call PopNavigation — a PushNavigation'd block has no owning element, so the built-in cancel handler leaves it in place:

if (MenuItem(ui, "settings", "Settings"))
    ui.PushNavigation("settings_menu");

ui.BeginNavigation("settings_menu", initialFocus: "volume");
{
    // settings items
    if (ui.IsNavigationActive("settings_menu") && ui.Bindings.Navigation.Cancel())
        ui.PopNavigation();
}
ui.EndNavigation();

The state of a block reflects in three queries:

  • IsActiveNavigationBlock(id): the named block is the innermost currently-open BeginNavigation scope (the top of the render stack).
  • IsNavigationActive(id): the given block is at the top of the navigation history.
  • IsNavigationFirstFrame(id): the block became active this frame.

What blocks the input affects

Directional inputs, confirm, and cancel apply only to the active block. Other blocks rendered the same frame keep their last focused element but do not respond to navigation actions. Button calls inside an inactive block still respond to the mouse path; they ignore confirm.

Pitfalls

A navigation element registers in the wrong block. : NavigationElement registers in the block at the top of the navigation stack at the time of the call. Calling it inside a nested BeginNavigation scope routes it to the nested block. Place BeginNavigation carefully around the elements that belong to it.

Directional input never moves between two elements. : Neither element falls inside the other's ninety-degree cone for the press, or both register in different blocks. Confirm the two elements register in the same block, and lay them closer to a cardinal direction relative to each other.

A diagonal between two elements never wins focus. : The 0.5 dot-product cone excludes elements more than ninety degrees off the input direction. Lay the focused widgets out closer to a cardinal direction relative to each other so the target falls inside the cone.

The built-in cancel does nothing. : The built-in handler pops a block only when it is the active one and it was entered by confirming an owning element. A block that is rendered but not active ignores cancel; so does a block entered by a plain PushNavigation. For the latter, read Bindings.Navigation.Cancel() in the app and call PopNavigation to step back.

Additional resources