Navigation
In this article, we'll cover a variety of ways Interactables and Scrollers can be configured to handle Navigation input. We'll also cover how to get started with sending navigation input (e.g. DPad/WASD) to Nova using both Unity's legacy Input APIs as well as Unity's new Input System Package. The exact code will depend on the input library you use, but the Nova-relevant code boils down to:
- Calling Navigation.Focus and providing a navigable UIBlock as a starting point to assign navigation focus.
- Calling Navigation.Move with the local direction in which to move navigation focus.
- Calling Navigation.Select to select the UIBlock which currently has navigation focus.
See Input Overview for an overview of Nova's input system.
Configuring Navigation
A navigation configuration consists of four key concepts:
Concept | Description | Uses |
---|---|---|
Navigable | Determines whether or not the object can receive navigation focus. | Opt in or out of which elements can receive navigation focus. |
Select Behavior | Tells the system which events to fire and what state to change when the focused object is Selected. | Fire a click event, scope navigation queries to descendants of the selected element, or receive direction-based input events to handle input more manually. |
Auto Select | Tells the system to automatically Select the object as soon as it receives navigation focus. | Reduce the number of explicit "select" actions the end-user needs to perform and simplify custom scripting for common navigation patterns. |
NavLink | Tells the system where to move navigation focus when navigating in a specific direction. Also exposes a fallback operation to perform in the event a navigation target isn't found in the specified direction. | Either attempt to navigate to the closest element automatically or manually configure a navigation target. |

Selection
If an Interactable or Scroller's On Select property is set to either Scope Navigation or Fire Events, the navigation system will push the object onto the selection stack whenever it's selected by the end-user. The object at the top of the selection stack is considered actively selected. Conversly, deselecting the actively selected object, either explicitly via Navigation.Deselect or implicitly by exiting the navigation scope (achievable using fallbacks or assigning focus outside the active scope), will pop it off the selection stack.
Having an object actively selected will adjust the behavior of Navigation.Move calls as follows:
If the active selection is configured to Fire Events, no focus changes will occur until it's deselected. Instead, the actively selected object will capture the provided input and fire corresponding Navigate.OnDirection events. The events can then be handled like any other gesture, enabling more custom behavior for a variety of UI controls and other unique use cases.
If the active selection is configured to Scope Navigation, subsequent Navigation.Move calls will be filtered to navigable descendants of the actively selected object, providing a way to support navigating "into" more complex controls or menus in your UI, rather than purely "across" them.
Some common On Select configurations to use when adding navigation support to existing pointer-based controls include but are not limited to:
UI Control | On Select | Additional Notes |
---|---|---|
Button | Click | None |
Toggle | Click | None |
Slider | Fire Events | Subscribe to Navigate.OnDirection events wherever the control's Gesture.OnDrag events are being handled and adjust the slider position based on the direction of input. |
Scrollable List | Scope Navigation | Enabling Auto Select may lead to a more streamlined end-user flow, depending on your desired use case. |
Note
When On Select is set to Click, no Navigate.OnSelect event is fired because the element is not in a "selected" state. Only a Gesture.OnClick event will fire in this scenario.
Visualizing Navigation
The Interactable and Scroller editors provide a way to enable a debug view of the navigation graph in its entirety, allowing you to visualize where the navigation system will move navigation focus between different elements in the scene.
Warning
Rendering the navigation graph can impact your editor performance with many navigable elements in the scene. Enabling Burst (paid version only) will substantially improve the performance of displaying the navigation graph. Alternatively, you can filter the graph to only render the subgraph of selected GestureRecognizers or disable it entirely when it's not needed.
Code Samples
Legacy Input APIs
using Nova;
using UnityEngine;
public class LegacyNavigationInputSample : MonoBehaviour
{
public const uint NavigationID = 3;
[Tooltip("The starting point from which to begin navigating.")]
public GestureRecognizer StartFrom = null;
[Tooltip("The UIBlock to move around as navigation focus changes. Will be created if null.")]
public UIBlock FocusIndicator = null;
[Header("Direction Keys")]
public KeyCode UpKey = KeyCode.UpArrow;
public KeyCode DownKey = KeyCode.DownArrow;
public KeyCode LeftKey = KeyCode.LeftArrow;
public KeyCode RightKey = KeyCode.RightArrow;
[Header("Action Keys")]
public KeyCode SelectKey = KeyCode.Return;
public KeyCode DeselectKey = KeyCode.Escape;
private void OnEnable()
{
if (StartFrom == null)
{
// No place to start. Can't begin navigating.
return;
}
EnsureFocusIndicator();
// Show the focus indicator
FocusIndicator.gameObject.SetActive(true);
// Subscribe to focus change events
Navigation.OnNavigationFocusChanged += HandleFocusChanged;
// Begin navigating
Navigation.Focus(StartFrom.UIBlock, NavigationID);
}
private void OnDisable()
{
// Unsubscribe from focus change events
Navigation.OnNavigationFocusChanged -= HandleFocusChanged;
if (FocusIndicator != null)
{
// Hide the focus indicator
FocusIndicator.gameObject.SetActive(false);
}
}
void Update()
{
if (!Navigation.TryGetFocusedUIBlock(NavigationID, out UIBlock focused))
{
// Nothing has navigation focus. Unable to navigate.
return;
}
if (Input.GetKeyUp(DownKey))
{
Navigation.Move(Vector3.down, NavigationID);
}
else if (Input.GetKeyUp(UpKey))
{
Navigation.Move(Vector3.up, NavigationID);
}
else if (Input.GetKeyUp(LeftKey))
{
Navigation.Move(Vector3.left, NavigationID);
}
else if (Input.GetKeyUp(RightKey))
{
Navigation.Move(Vector3.right, NavigationID);
}
else if (Input.GetKeyUp(DeselectKey))
{
Navigation.Deselect(NavigationID);
}
else if (Input.GetKeyUp(SelectKey))
{
Navigation.Select(NavigationID);
}
}
private void HandleFocusChanged(uint controlID, UIBlock focused)
{
if (FocusIndicator == null)
{
// Don't have an indicator. Nothing to do.
return;
}
// Hide indicator if nothing is focused
FocusIndicator.gameObject.SetActive(focused != null);
if (focused == null)
{
// Don't have a size/position to move
return;
}
// Match the world scale of the focused element
Vector3 parentScale = FocusIndicator.transform.parent == null ? Vector3.one : FocusIndicator.transform.parent.lossyScale;
Vector3 focusedScale = focused.transform.lossyScale;
FocusIndicator.transform.localScale = new Vector3(focusedScale.x / parentScale.x,
focusedScale.y / parentScale.y,
focusedScale.z / parentScale.z);
// Update size and position to match whatever's focused
FocusIndicator.Size = focused.CalculatedSize.Value + FocusIndicator.CalculatedPadding.Size;
FocusIndicator.TrySetWorldPosition(focused.transform.position);
FocusIndicator.transform.rotation = focused.transform.rotation;
}
private void EnsureFocusIndicator()
{
if (FocusIndicator != null)
{
return;
}
// Create a new game object to be our focus indicator
UIBlock2D indicator = new GameObject("Focus Indicator").AddComponent<UIBlock2D>();
// Hide the body and enable the border
indicator.BodyEnabled = false;
indicator.Border.Enabled = true;
indicator.Border.Direction = BorderDirection.Out;
indicator.Border.Width = 5;
// Add some padding
indicator.Padding.XY = 2;
// Make sure the indicator will render over the other content
SortGroup sortGroup = indicator.gameObject.AddComponent<SortGroup>();
sortGroup.RenderQueue = 4001;
sortGroup.RenderOverOpaqueGeometry = true;
// assign the focus indicator
FocusIndicator = indicator;
}
}
Input System Package
using Nova;
using UnityEngine;
using UnityEngine.InputSystem;
public class NavigationInputSample : MonoBehaviour
{
public const uint NavigationID = 3;
[Tooltip("The starting point from which to begin navigating.")]
public GestureRecognizer StartFrom = null;
[Tooltip("The UIBlock to move around as navigation focus changes. Will be created if null.")]
public UIBlock FocusIndicator = null;
[Header("Direction Keys")]
public Key UpKey = Key.UpArrow;
public Key DownKey = Key.DownArrow;
public Key LeftKey = Key.LeftArrow;
public Key RightKey = Key.RightArrow;
[Header("Action Keys")]
public Key SelectKey = Key.Enter;
public Key DeselectKey = Key.Escape;
private void OnEnable()
{
if (StartFrom == null)
{
// No place to start. Can't begin navigating.
return;
}
// Create the focus indicator if it doesn't already exist
EnsureFocusIndicator();
// Show the focus indicator
FocusIndicator.gameObject.SetActive(true);
// Subscribe to focus change events
Navigation.OnNavigationFocusChanged += HandleFocusChanged;
// Begin navigating
Navigation.Focus(StartFrom.UIBlock, NavigationID);
}
private void OnDisable()
{
// Unsubscribe from focus change events
Navigation.OnNavigationFocusChanged -= HandleFocusChanged;
if (FocusIndicator != null)
{
// Hide the focus indicator
FocusIndicator.gameObject.SetActive(false);
}
}
void Update()
{
if (Keyboard.current == null)
{
// Keyboard not found. Unable to navigate.
return;
}
if (!Navigation.TryGetFocusedUIBlock(NavigationID, out UIBlock focused))
{
// Nothing has navigation focus. Unable to navigate.
return;
}
if (Keyboard.current[DownKey].wasReleasedThisFrame)
{
Navigation.Move(Vector3.down, NavigationID);
}
else if (Keyboard.current[UpKey].wasReleasedThisFrame)
{
Navigation.Move(Vector3.up, NavigationID);
}
else if (Keyboard.current[LeftKey].wasReleasedThisFrame)
{
Navigation.Move(Vector3.left, NavigationID);
}
else if (Keyboard.current[RightKey].wasReleasedThisFrame)
{
Navigation.Move(Vector3.right, NavigationID);
}
else if (Keyboard.current[DeselectKey].wasReleasedThisFrame)
{
Navigation.Deselect(NavigationID);
}
else if (Keyboard.current[SelectKey].wasReleasedThisFrame)
{
Navigation.Select(NavigationID);
}
}
private void HandleFocusChanged(uint controlID, UIBlock focused)
{
if (FocusIndicator == null)
{
// Don't have an indicator. Nothing to do.
return;
}
// Hide indicator if nothing is focused
FocusIndicator.gameObject.SetActive(focused != null);
if (focused == null)
{
// Don't have a size/position to move
return;
}
// Match the world scale of the focused element
Vector3 parentScale = FocusIndicator.transform.parent == null ? Vector3.one : FocusIndicator.transform.parent.lossyScale;
Vector3 focusedScale = focused.transform.lossyScale;
FocusIndicator.transform.localScale = new Vector3(focusedScale.x / parentScale.x,
focusedScale.y / parentScale.y,
focusedScale.z / parentScale.z);
// Update size and position to match whatever's focused
FocusIndicator.Size = focused.CalculatedSize.Value + FocusIndicator.CalculatedPadding.Size;
FocusIndicator.TrySetWorldPosition(focused.transform.position);
FocusIndicator.transform.rotation = focused.transform.rotation;
}
private void EnsureFocusIndicator()
{
if (FocusIndicator != null)
{
return;
}
// Create a new game object to be our focus indicator
UIBlock2D indicator = new GameObject("Focus Indicator").AddComponent<UIBlock2D>();
// Hide the body and enable the border
indicator.BodyEnabled = false;
indicator.Border.Enabled = true;
indicator.Border.Direction = BorderDirection.Out;
indicator.Border.Width = 5;
// Add some padding
indicator.Padding.XY = 2;
// Make sure the indicator will render over the other content
SortGroup sortGroup = indicator.gameObject.AddComponent<SortGroup>();
sortGroup.RenderQueue = 4001;
sortGroup.RenderOverOpaqueGeometry = true;
// assign the focus indicator
FocusIndicator = indicator;
}
}