Skip to content

Build a navigable menu

A navigable menu lets the player move focus between items with directional input, confirm the focused item, and cancel back out of the menu. The pattern combines BeginNavigation, NavigationElement, IsFocused, and Button.

For the model behind navigation, refer to Navigation. For the inputs that move focus and trigger confirm and cancel, refer to Input bindings.

To build a navigable menu

  1. Wrap the menu in BeginNavigation. Pass active: true and an initialFocus id so a default element focuses on the first frame:
    ui.BeginNavigation("main_menu", active: true, initialFocus: "new_game");
    {
        // items go here
    }
    ui.EndNavigation();
    
  2. Lay the items out in the visual order navigation should follow. PushSeriesVertical (or any layout schema) defines the geometry directional scoring uses to pick the target for each direction:
    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();
    }
    
  3. Register each item with NavigationElement and draw a focus visual through IsFocused. Button reads Bindings.Navigation.Confirm(), so a focused button activates on the frame the confirm input fires:
    static bool MenuItem(UI ui, string id, string label)
    {
        ui.NavigationElement(id);
        ui.FillRounded(ui.IsFocused(id) ? focusColor : restColor, 5);
        ui.Text(label, "roboto_mono", 16, textColor, TextAlign.MiddleCenter);
        return ui.Button(id);
    }
    

The menu is now navigable. Directional input moves focus between the registered items; confirm activates the focused item. To handle cancel, read Bindings.Navigation.Cancel() in the menu body and call PopNavigation — see Open a submenu.

Open a submenu

A submenu becomes its own navigation block. Push it from the outer menu so cancel returns to the parent:

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

ui.BeginNavigation("settings_menu", initialFocus: "volume");
{
    using (ui.Auto().Pad(20).PushSeriesVertical(32, 5))
    {
        if (MenuItem(ui, "volume",     "Volume"))     OpenVolume();
        ui.Next();
        if (MenuItem(ui, "key_binds",  "Key binds"))  OpenKeyBinds();
    }
    if (ui.IsNavigationActive("settings_menu") && ui.Bindings.Navigation.Cancel())
        ui.PopNavigation();
}
ui.EndNavigation();

BeginNavigation("settings_menu", …) declares the inner block without activating it. PushNavigation("settings_menu") activates the block on the next frame and saves the outer block's focused element. A PushNavigation'd block has no owning element, so the built-in cancel handler leaves it in place; read Bindings.Navigation.Cancel() in the submenu body and call PopNavigation to step back to the outer block, which restores its previous focus.

IsNavigationFirstFrame("settings_menu") reads true on the first frame after the push, which is the right hook for playing an open animation or making a sound.

Combine navigation with the mouse

A widget can be both navigable and clickable. Button returns true on a confirmed-focus press or on a mouse click. The calling code does not need to know which path triggered it. Hover state still drives the visual when the mouse is present:

Color tint =
    ui.IsHovered() ? hoverColor :
    ui.IsFocused(id) ? focusColor :
    restColor;
ui.FillRounded(tint, 5);

Mouse hover and navigation focus stay independent. Moving the mouse over an item does not change focus, and moving focus does not warp the cursor.

Pitfalls

Direction never moves on the first frame. : No element holds focus. Pass initialFocus to BeginNavigation, or call SetFocus(id) manually after the first frame the block is active.

Two adjacent items skip each other on a directional press. : The projected direction from the focused element to the neighbour does not fall inside the 0.5 dot-product cone, or a third element scores higher. Lay the items closer to a cardinal direction relative to each other.

Cancel exits the wrong menu. : The built-in cancel pops only a block entered by confirming an owning element, returning to that element. For a PushNavigation'd submenu, the app drives the back step by reading Bindings.Navigation.Cancel() and calling PopNavigation. Guard that call so it does not reach the root block when no submenu is active, or the root pops with nothing beneath it.

A confirmed button fires at the same time as a mouse click. : Both paths trigger Button. The widget treats it as a single activation. To distinguish the source, read Bindings.Navigation.Confirm() or ui.IsHovered() directly instead of relying on Button's return value.

Additional resources