Skip to content

Navigation

The Navigation component is a comprehensive navigation system designed for game UIs. It provides keyboard and gamepad input handling, spatial navigation between UI areas, and a flexible action system for mapping inputs to callbacks. Built on top of the Coherent Gameface Interaction Manager, it enables seamless navigation across complex menu systems and HUD elements.

This component provides:

  • Keyboard and gamepad input management with customizable bindings
  • Spatial navigation with multiple navigation areas
  • Action system for mapping inputs to game-specific callbacks
  • Scope-based context awareness for different UI sections
  • Pause/resume functionality for navigation state
  • Integration with components via context and navigation actions directive

To use the Navigation component, wrap your UI with it and define navigation areas for different sections of your UI. You can configure default actions and provide custom input bindings for your game.

import Navigation from '@components/Navigation/Navigation/Navigation';
import { ActionMap } from '@components/Navigation/Navigation/types';
const App = () => {
const defaultActions: ActionMap = {
'tab-left': {key: {binds: ['Q'], type: ['press']}, button: {binds: ['left-sholder'], type: 'press'}, callback: menuLeft, global: true},
'tab-right': {key: {binds: ['E'], type: ['press']}, button: {binds: ['right-sholder'], type: 'press'}, callback: menuRight, global: true},
'select': {key: {binds: ['SPACE'], type: ['press']}, button: {binds: ['face-button-left'], type: 'press'}},
'back': {key: {binds: ['BACKSPACE'], type: ['press']}},
}
return (
<Navigation scope="main-menu" actions={defaultActions} pollingInterval={150} >
<Navigation.Area name="main-menu" focused>
<button>Start Game</button>
<button>Settings</button>
<button>Quit</button>
</Navigation.Area>
</Navigation>
);
};
export default App;

The Navigation component comes with a couple of pre-configured default actions that handle common navigation patterns in game UIs. These actions are automatically registered when the Navigation component mounts. However, components typically listen for these actions only when they are focused.

For example, pressing Arrow Right emits a global move-right event, but only the currently focused Stepper will respond to it. This ensures that you don’t accidentally change settings in a background menu while navigating the main screen.

These actions are recognized across the entire Gameface UI component library, enabling preset components to work seamlessly with the navigation system out of the box.

Each default action has:

  • Keyboard bindings - One or more keyboard keys
  • Gamepad bindings - One or more gamepad buttons
  • Global emission - They emit events via the event bus, allowing any component to respond
  • No default callbacks - They don’t execute callbacks by default, only emit events. However they can be extended to execute callback as well.
  • Library-wide recognition - Preset components like Dropdown, Stepper, Checkbox, and others automatically listen to these actions
Action NameKeyboard BindingGamepad BindingPurpose
move-leftARROW_LEFTpad-left (D-Pad)Directional input left (used by components Stepper)
move-rightARROW_RIGHTpad-right (D-Pad)Directional input right (used by components Stepper)
move-upARROW_UPpad-up (D-Pad)Directional input up (used by components like Dropdown)
move-downARROW_DOWNpad-down (D-Pad)Directional input down (used by components like Dropdown)
selectENTERface-button-downConfirm selection or activate the currently focused element
backESCface-button-rightCancel current operation or navigate back to the previous screen
pannoneright.joystickContinuous 2D input for manipulating values on two axes simultaneously. Paused by default

Default actions can be customized by updating their configuration. This is useful when you want to:

  • Change keyboard/gamepad bindings to match your game’s control scheme
  • Add custom callbacks to default actions
  • Modify input types (press, hold, lift)

You can customize default actions either through the actions prop or programmatically using updateAction():

Via props:

import Navigation from '@components/Navigation/Navigation/Navigation';
import { ActionMap } from '@components/Navigation/Navigation/types';
const customActions: ActionMap = {
// Customize the 'select' action to use SPACE instead of ENTER
'select': {
key: {binds: ['SPACEBAR'], type: ['press']},
button: {binds: ['face-button-down'], type: 'press'},
callback: (scope) => console.log('Selected in', scope),
},
// Customize 'back' to add a callback
'back': {
key: {binds: ['ESC'], type: ['press']},
button: {binds: ['face-button-right'], type: 'press'},
callback: () => handleBack(),
}
};
<Navigation actions={customActions}>
{/* Your UI */}
</Navigation>

Programmatically:

const nav = useNavigation();
// Update the select action at runtime
nav.updateAction('select', {
key: {binds: ['SPACEBAR'], type: ['press']},
button: {binds: ['face-button-down'], type: 'press'},
callback: (scope) => handleSelection(scope),
global: true
});

Beyond the default actions, you can add your own custom actions for game-specific inputs:

import Navigation from '@components/Navigation/Navigation/Navigation';
import { ActionMap } from '@components/Navigation/Navigation/types';
const customActions: ActionMap = {
// Custom action for opening inventory
'open-inventory': {
key: {binds: ['I'], type: ['press']},
button: {binds: ['face-button-top'], type: 'press'},
callback: () => openInventory(),
global: true
},
// Custom action for opening map
'open-map': {
key: {binds: ['M'], type: ['press']},
button: {binds: ['face-button-left'], type: 'press'},
callback: (scope) => openMap(scope),
global: false // Only emit locally, not globally
}
};
<Navigation actions={customActions}>
{/* Your UI */}
</Navigation>

Or add them programmatically:

const nav = useNavigation();
nav.addAction('toggle-menu', {
key: {binds: ['TAB'], type: ['press']},
button: {binds: ['START'], type: 'press'},
callback: () => toggleMenu(),
global: true
});

Some components may need to respond to actions when a different element is focused. You can achieve this by specifying an anchor element in the action configuration. When the anchor element is focused, the action will trigger for the specified element.

In the following example, the Stepper component will respond to navigation actions when the parent .menu-item div is focused.

<div id="difficulty" class="menu-item">
<div>Select Difficulty:</div>
<Stepper anchor=".menu-item" onChange={emitChange}>
<Stepper.Items>
<Stepper.Item value='Easy'>Easy</Stepper.Item>
<Stepper.Item value='Normal' selected>Normal</Stepper.Item>
<Stepper.Item value='Hard'>Hard</Stepper.Item>
<Stepper.Item value='Nightmare'>Nightmare</Stepper.Item>
</Stepper.Items>
<Stepper.Control />
</Stepper>
</MenuItem>

This functionality is especially useful for more complex component configurations where the component doesn’t directly receive focus but still need to respond to navigation actions.

Subscribing to Actions in Custom Components

Section titled “Subscribing to Actions in Custom Components”

Custom components can respond to navigation actions using the navigationActions directive from BaseComponent. This directive automatically listens for action events and executes callbacks when the component (or its anchor) is focused.

To subscribe your component to an action, follow these steps:

  1. Import the navigationActions method in your component

  2. Add the navigationActions method as an attribute to the element you wish to subscribe, and prefix it with the use: keyword

  3. Provide the names of the actions you wish to subscribe to and their corresponding callbacks (what will happen when they’re triggered)

import { navigationActions } from '@components/BaseComponent/BaseComponent';
const MyComponent = (props) => {
return (
<div use:navigationActions={{
select: () => console.log('Item selected'),
back: () => console.log('Going back')
}}>
</div>
);
};

With an anchor element:

Anchor can also be used when subscribing to actions in custom components.

const Card = (props) => {
let buttonRef;
return (
<div
use:navigationActions={{
anchor: buttonRef, // Respond when button is focused
select: () => handleCardSelect(),
'custom-action': () => handleCustomAction()
}}
>
<button ref={buttonRef}>Select</button>
<div>Card content</div>
</div>
);
};

Many preset components in the Gameface UI library (such as Stepper, Dropdown, etc.) come with predefined navigation action handlers. For example, the Stepper component responds to move-left and move-right actions by default.

You can extend or override these default behaviors using the onAction prop available on all components that extend ComponentProps. This allows you to:

  • Add additional action handlers to components (e.g., adding a select action to a Stepper)
  • Override the component’s default action behavior
  • Customize how components respond to navigation inputs

Example - Adding a select action to a Stepper:

import Stepper from '@components/Basic/Stepper/Stepper';
<Stepper
onAction={{
'select': () => console.log('Stepper item confirmed!')
}}
>
<Stepper.Items>
<Stepper.Item value="option1">Option 1</Stepper.Item>
<Stepper.Item value="option2">Option 2</Stepper.Item>
</Stepper.Items>
</Stepper>

Example - Overriding default behavior:

<Stepper
onAction={{
'move-left': () => customPreviousLogic(), // Overrides default
'select': () => handleSelection() // Extends default
}}
>
{/* ... */}
</Stepper>

Action Configuration Reference (ActionCfg)

Section titled “Action Configuration Reference (ActionCfg)”

When defining actions, you can configure the following properties:

PropertyTypeDescription
key{binds: KeyName[], type?: ActionType[]}Keyboard configuration. binds specifies which keys trigger the action. type specifies when to trigger: 'press' (default), 'hold', or 'lift'.
button{binds: GamepadInput[], type?: Exclude<ActionType, 'lift'>}Gamepad configuration. binds specifies which buttons trigger the action. type specifies when to trigger: 'press' (default) or 'hold'.
callback(scope?: string, ...args: any[]) => voidFunction to execute when the action is triggered. Receives the current navigation scope as the first parameter.
globalbooleanWhen true, the action emits globally via the eventBus, allowing any component to listen. When false, only the callback is executed. Default actions have this set to true.
pausedbooleanWhen set to true the action will be paused by default. Useful for configuring actions that are not immediately needed.
Prop NameTypeDefaultDescription
gamepadbooleantrueEnables gamepad input handling for navigation and actions.
keyboardbooleantrueEnables keyboard input handling for navigation and actions.
actionsActionMap{}Custom action configurations to register in addition to default actions. Each action can define keyboard/gamepad bindings and callbacks.
scopestring""The initial navigation scope. When a Navigation.Area with a matching name is registered, it will be auto-focused.
pollingIntervalnumber200Gamepad polling interval in milliseconds. Determines how frequently the system checks for gamepad input.
refNavigationRef | ((nav: NavigationRef) => void)undefinedA reference to the component, providing access to all navigation methods via the NavigationRef interface.
overlapnumberundefinedOverlap threshold for spatial navigation. Determines how much elements can overlap before being considered in different navigation paths.

The Navigation component exposes a comprehensive API through the useNavigation() hook (via context) or through a ref. This API provides methods for managing navigation areas, handling actions, and controlling navigation state.

Method NameParametersReturn ValueDescription
addActionname: ActionName, config: ActionCfgvoidAdds a new Navigation action and registers it with the interaction manager.
removeActionname: ActionNamevoidRemoves a registered Navigation action and unregisters it from the interaction manager.
updateActionname: ActionName, config: ActionCfgvoidUpdates an existing action’s configuration. Default actions can be updated as well. Accepts the same config properties as addAction.
executeActionname: ActionNamevoidExecutes a registered action by name, triggering its callback and event emission.
pauseActionaction: ActionName, force?: boolean = falsevoidPauses an action, preventing its callback from executing. Actions can be paused only when they don’t have subscribers, to bypass that provide true as the second argument.
resumeActionaction: ActionName, force?: boolean = falsevoidResumes a paused action, allowing its callback to execute again. If an action was force paused, you must resume it by providing true as a second argument when calling the function.
isPausedaction: ActionNamebooleanChecks if an action is currently paused. Returns true if paused, false otherwise.
getScopeNonestringGets the current navigation scope (typically the name of the active navigation area).
getActionname: ActionNameActionCfg | undefinedGets a specific action configuration by name. Returns undefined if the action doesn’t exist.
getActionsNoneActionMapGets all currently registered actions.
pauseInputNonevoidSnapshots current pause states and forcefully pauses all actions (ideal for stopping all action input). resumeInput must be used to unpause all actions.
resumeInputNonevoidReleases the global pause and restores actions to their state prior to the pauseInput call.
Method NameParametersReturn ValueDescription
registerAreaarea: string, elements: string[] | HTMLElement[], focused?: booleanvoidRegisters a navigation area with focusable elements. If focused is true, automatically focuses the first element in the area.
unregisterAreaarea: stringvoidUnregisters a navigation area and removes it from spatial navigation.
focusFirstarea: stringvoidFocuses the first focusable element in the specified area and updates the navigation scope.
focusLastarea: stringvoidFocuses the last focusable element in the specified area and updates the navigation scope.
switchAreaarea: stringvoidSwitches the active navigation to the specified area by focusing the first element in it and updating the navigation scope.
clearFocusNonevoidClears the current focus from all navigation areas.
changeNavigationKeyskeys: { up?: string, down?: string, left?: string, right?: string }, clearCurrent?: booleanvoidChanges the navigation keys for spatial navigation. If clearCurrent is true, clears current active keys before setting new ones.
resetNavigationKeysNonevoidResets navigation keys to their default values.
pauseNavigationNonevoidPauses navigation, preventing spatial navigation actions from executing.
resumeNavigationNonevoidResumes navigation, allowing spatial navigation actions to execute again.

The Navigation component exposes subcomponents that allow you to structure your navigation:

Navigation.Area registers a section of your UI as a navigable area. Elements within the area can be navigated using keyboard/gamepad inputs. Areas can be switched programmatically.

When a Navigation.Area mounts:

  • It registers itself with the spatial navigation system
  • If its name matches the parent Navigation’s scope prop or if focused={true}, it auto-focuses
  • It automatically handles cleanup on unmount
Prop NameTypeDefaultDescription
namestringrequiredThe unique name identifier for this navigation area. Used to reference the area in methods like focusFirst, switchArea, and for scope tracking.
selectorstringundefinedCSS class selector for navigable elements. If provided, only elements matching this selector will be navigable. If omitted, all child elements are considered navigable.
focusedbooleanfalseWhen true, this area will automatically receive focus when it mounts, focusing its first navigable element.
<Navigation scope="settings">
<Navigation.Area name="settings" focused>
<button>Graphics</button>
<button>Audio</button>
<button>Controls</button>
</Navigation.Area>
<Navigation.Area name="graphics-settings" selector="setting-option">
<div class="setting-option">Resolution</div>
<div class="setting-option">Quality</div>
<div class="setting-option">V-Sync</div>
</Navigation.Area>
</Navigation>

You can access the Navigation component’s methods and properties from within your components in 2 ways:

  1. Using the useNavigation() hook

This hook provides access to the navigation API via context. It is the recommended method for any child component nested inside Navigation.

import { useNavigation } from "@components/Utility/Navigation/Navigation";
import Button from '@components/Basic/Button/Button';
const MyComponent = () => {
const navigation = useNavigation();
// Use navigation methods here
return (
<Button onAction={{'select': () => navigation.focusFirst('main-menu')}}>
Focus Main Menu
</Button>
);
};
  1. Using a ref to the Navigation component

For components that use the Navigation directly in the return block, you cannot use the hook. Instead, you must attach a ref.

Option A: The Callback Ref (Recommended for Initialization)

If you need to execute logic immediately when the navigation mounts (e.g., registering default areas), use the callback pattern. This guarantees the API is ready before you use it.

import Navigation, { NavigationRef } from "@components/Utility/Navigation/Navigation";
const MyComponent = () => {
// 1. Store it if you need it for later (clicks/events)
let navigationRef: NavigationRef | undefined;
// 2. Define the callback that runs immediately on mount
const handleNavReady = (api: NavigationRef) => {
navigationRef = api; // Save reference
// Safe to use API immediately here
api.registerArea('main-menu', ['#start-button', '#settings-button'], true);
}
return (
<Navigation ref={handleNavReady}>
{/* Your UI */}
</Navigation>
);
};

Option B: The Simple Ref (For Event Handlers Only)

If you only need the API for user interactions that happen after the page loads, you can use a simple variable assignment.

import Navigation, { NavigationRef } from "@components/Utility/Navigation/Navigation";
const MyComponent = () => {
let navigationRef: NavigationRef | undefined;
const someHandler = () => {
navigationRef?.focusFirst('main-menu');
}
return (
<Navigation ref={navigationRef}>
{/* Your UI */}
</Navigation>
);
};

Actions can be paused and resumed to control when they are active. This is useful for optimizing performance or managing context-specific inputs.

For example, you might want to pause a certain action to avoid a case where a button press triggers multiple actions in a specific context. In such cases, you can pause the action when entering that context and resume it when exiting.

import { useNavigation } from "@components/Utility/Navigation/Navigation";
const MyComponent = () => {
const navigation = useNavigation();
const openMenu = () => {
// Pause the actions to prevent accidental component triggers
navigation.pauseAction('move-left');
navigation.pauseAction('move-right');
};
const closeMenu = () => {
// Resume the actions when leaving the menu
navigation.resumeAction('move-left');
navigation.resumeAction('move-right');
};
return (
<div>
<button onClick={openMenu}>Open Menu</button>
<button onClick={closeMenu}>Exit Menu</button>
</div>
);
};

In this example, the move-left and move-right actions are paused when entering a specific context (e.g., a modal or special menu) and resumed when exiting that context.

Internally resumeAction and pauseAction manage a counter that prevents desynchronization when multiple components try to pause/resume the same action. For example if two components pause the same action, it will require two calls to resumeAction to resume it.

This is done to avoid cases where one component resumes an action that another component still needs paused. Another common case is if you have two component that use the pan action - both will resume it when mounted and pause it when unmounted. Without the counter, unmounting one component will pause the action even if the other component is still mounted, prevent it from reacting to the action.

However, if you need to bypass this behavior, you can provide true as the second argument to both methods to force the action to pause or resume immediately.

const MyComponent = () => {
const navigation = useNavigation();
const forcePause = () => {
// Force pause the 'select' action immediately
navigation.pauseAction('select', true);
};
const forceResume = () => {
// Force resume the 'select' action immediately
navigation.resumeAction('select', true);
};
return (
<div>
<button onClick={forcePause}>Force Pause Select</button>
<button onClick={forceResume}>Force Resume Select</button>
</div>
);
};