Skip to content

Focus Navigation

The FocusNavigation package contains Luau-based helpers intended to support more complex directional navigation, like with a keyboard or gamepad, in a generic way.

Types

Events

FocusNavigation re-exports these event types from the EventPropagation module:

EventMap

type EventMap = {
    [Enum.KeyCode]: string,
}

A mapping of input KeyCodes to event names. When an EventMap is registered on a GuiObject via FocusNavigationService:registerEventMap, the named events will be fired and propagated when the given input is observed.

EventData

type EventData = {
    Delta: Vector3,
    KeyCode: Enum.KeyCode,
    Position: Vector3,
    UserInputState: Enum.UserInputState,
    UserInputType: Enum.UserInputType,
    wasProcessed: boolean?,
}

type SyntheticEventData = EventData & {
    immediateDispatch: boolean?,
}

EventData is a union type that can contain either natural input event data (InputEvent) or synthetic event data (SyntheticEventData). It's accessed through the eventData field of the Event passed to an EventHandler.

InputEvent

Contains properties from the input InputObject provided to the input event callback, representing natural user input.

SyntheticEventData

Represents data for events created programmatically via dispatchSyntheticEvent(). It extends InputEvent with additional synthetic-specific fields:

  • immediateDispatch: When true, causes synthetic events to bypass normal input timing logic and execute handlers immediately. This is useful for tappable shortcuts that should fire instantly without hold delays.

ContainerFocusBehavior

type ContainerFocusBehavior = {
    onDescendantFocusChanged: (GuiObject?) -> (),
    getTarget: () -> GuiObject?,
}

The ContainerFocusBehavior type represents a set of FocusBehavior rules that will be applied to a container GuiObject. The two functions are callbacks triggered by the FocusNavigationService to help redirect selection when a container gains focus.

This is useful for things like declaring a default descendant to be focused when a page gains focus, or tracking the most recently focused descendant and restoring it when returning from a modal.

onDescendantFocusChanged

function onDescendantFocusChanged(focusedDescendant: GuiObject?)

This function will be called any time focus changes within a given container. It can be used to track focus history within a container so that it can be restored in the future.

If the focus is redirected from its initial target, this callback will only be fired with the most recent focus target, not the one that was redirected away from.

getTargets

function getTargets(): { GuiObject }

Returns an array of GuiObject candidates that should gain focus when focus moves from outside of the container to into it. This function's return dictates the initial or restored focus state that the ContainerFocusBehavior will redirect to. The FocusNavigationService will attempt to validate each member in the provided order, using isValidFocusTarget, and redirect focus to the first valid one. If the list is empty or no members are valid, focus will not be redirected.

Package API

FocusNavigationService

type FocusNavigationService = FocusNavigation.FocusNavigationService

Exports the FocusNavigationService object, which can be instantiated using the static new function described below.

EngineInterface

type EngineInterface = {
    CoreGui = FocusNavigation.EngineInterfaceType,
    PlayerGui = FocusNavigation.EngineInterfaceType,
}

Provides the two possible engine interface modes for the FocusNavigationService. These interfaces abstract over engine functionality that the FocusNavigationService needs to use under the hood, such as distinguishing between GuiService.SelectedObject and GuiService.SelectedCoreObject.

isValidFocusTarget

function FocusNavigation.isValidFocusTarget(target: Instance?) -> (boolean, string?)

Returns a boolean representing whether or not the target is capable of receiving focus via Roblox engine selection (i.e. "Can GuiService.Selected(Core)Object be set to this value?").

If the function returns false, the second return value will contain an error string explaining why the target was not a valid focus target. This can be ignored or escalated as a warning or error message if needed.

Service API

As mentioned above, the focus-navigation package exports FocusNavigationService, which is the workhorse of this library. It contains all the public methods which are used to build other focus utilities, like the ones in react-focus-navigation.

new

function FocusNavigationService.new(engineInterface: FocusNavigation.EngineInterfaceType)

Create a new FocusNavigationService. Intended only to be called once. Provide the relevant EngineInterface for the context:

  • use EngineInterface.CoreGui to manage focus for UI mounted under the CoreGui service
  • use EngineInterface.PlayerGui to manage focus for UI mounted under a Player instance's PlayerGui child

registerEventMap

function FocusNavigationService:registerEventMap(
    guiObject: GuiObject,
    eventMap: EventMap,
)

Register a mapping of input KeyCodes to event names for a given GuiObject. Event names can be tied to event handlers with registerEventHandler or registerEventHandlers.

Using semantic event names means that inputs can have generalized meanings that apply contextually via handlers for various parts of the application.

deregisterEventMap

function FocusNavigationService:deregisterEventMap(
    guiObject: GuiObject,
    eventMap: EventMap,
)

Deregister a set of event mappings for the given GuiObject.

registerEventHandler

function FocusNavigationService:registerEventHandler(
    guiObject: GuiObject,
    eventName: string,
    eventHandler: EventHandler<FocusNavigationEventData>,
    phase: EventPhase?
)

Register an individual EventHandler. This requires a GuiObject (the event will only fire when that GuiObject is focused), the event handler itself, and the name of the event. The event's name will be meaningful within the context of the application. An EventPhase optionally can be passed in to indicate which event propagation phase the handler should be triggered in, this defaults to "Bubble".

registerEventHandlers

function FocusNavigationService:registerEventHandlers(
    guiObject: GuiObject,
    map: EventHandlerMap<FocusNavigationEventData>
)

Register multiple EventHandlers from an instance using an EventHandlerMap.

deregisterEventHandler

function FocusNavigationService:deregisterEventHandler(
    guiObject: GuiObject,
    eventName: string,
    eventHandler: EventHandler<FocusNavigationEventData>,
    phase: EventPhase?
)

Deregister a single EventHandler from an GuiObject based on a phase. If phase is not passed in it defaults to "Bubble".

deregisterEventHandlers

function FocusNavigationService:deregisterEventHandlers(
    guiObject: GuiObject,
    map: EventHandlerMap<FocusNavigationEventData>
)

Deregister multiple EventHandlers from an instance using an EventHandlerMap.

registerFocusBehavior

function FocusNavigationService:registerFocusBehavior(
    guiObject: GuiObject,
    containerFocusBehavior: ContainerFocusBehavior,
)

Register a ContainerFocusBehavior on the given GuiObject container. Whenever focus moves from outside of that container to inside of that container, the behavior will trigger and redirect focus if a new target is provided.

Additionally, the onDescendantFocusChanged callback on the provided behavior will be fired every time focus changes to a descendant, either from outside the container, nil (nothing focused at all), or from another descendant inside the container. It will not be fired when focus moves out of the container.

Only one behavior can be registered on a given container object, so registering a new behavior without deregistering the old one will overwrite the old one (and issue a warning in DEV mode). If multiple behaviors should be combined, use the composeFocusBehaviors utility to order them appropriately.

deregisterEventHandler

function FocusNavigationService:deregisterFocusBehavior(
    guiObject: GuiObject,
    containerFocusBehavior: ContainerFocusBehavior,
)

Deregisters a ContainerFocusBehavior from a given GuiObject container. This means that no additional processing will occur when focus moves into the container, and focus will otherwise behave as dictated by the engine and any relevant Instance properties.

dispatchSyntheticEvent

function FocusNavigationService:dispatchSyntheticEvent(
    eventName: string,
    target: GuiObject,
    customEventData: EventData?
): boolean

Programmatically dispatches a synthetic event on the specified target GuiObject. Synthetic events behave like user-triggered events but can bypass normal input timing logic for immediate execution.

Returns true if the event was successfully dispatched, or false if the event could not be dispatched. The dispatch will fail if: - The target GuiObject is invalid - No event map is registered for the target - The specified eventName is not found in the target's event map
- No event handler is registered for the event

The customEventData parameter allows passing additional data to the event handler. If provided, it will be merged with default synthetic event data. The synthetic flag is automatically set to true on the Event object itself.

This method is primarily used internally by the useDispatchSyntheticEvent React hook, but can be called directly for non-React use cases.

canDispatchEvent

function FocusNavigationService:canDispatchEvent(
    eventName: string,
    target: GuiObject
): boolean

Validates whether a synthetic event can be dispatched on the specified target. Returns true if all prerequisites are met: - The target GuiObject is valid - An event map is registered for the target - The specified eventName exists in the target's event map - An event handler is registered for the event

This method is useful for pre-validation before attempting to dispatch synthetic events, allowing UI components to show/hide trigger elements based on whether events can be dispatched.

focusGuiObject

function FocusNavigationService:focusGuiObject(
    guiObject: GuiObject,
    silent: boolean
)

Move focus to the target GuiObject. Providing a value of true for the silent argument will suppress event capturing and bubbling, triggering registered events only for the target guiObjects themselves (both the previous focus and the new one).

Observable Fields

FocusNavigationService exposes two important observable properties.

activeEventMap

FocusNavigationService.activeEventMap: Signal<GuiObject?>

An observable property that provides the currently active mapping of input KeyCodes to focus navigation events. The active event map is composed from all events associated with the currently-focused GuiObject and its ancestors, where elements deeper in the tree will override events bound to their ancestors.

Subscribe to this value with a function as per the Signal API.

local subscription = FocusNavigation.activeEventMap:subscribe({
    next = function(evenMap)
        for keyCode, event in eventMap do
            print(string.format("trigger %s when pressing %s", event, tostring(keyCode))
        end
    end
})

focusedGuiObject

FocusNavigationService.focusedGuiObject: Signal<GuiObject?>

An observable property that tracks the currently focused instance. This is similar to connecting directly to the GuiService:GetPropertyChanged signal for the relevant property, but provides a nicer interface and automatically connects to the right GuiService property.

Subscribe to this value with a function as per the Signal API.

local subscription = FocusNavigation.focusedGuiObject:subscribe({
    next = function(newFocus)
        print(if newFocus then newFocus.Name else "(None)")
    end
})

Advanced Usage

For more advanced / systemic usage examples of the focus-navigation module, check out the focus navigation DemoApp in this library's GitHub repo.