React Focus Navigation
React Focus Navigation¶
A library for handling UI interactions with directional input, like gamepad or keyboard, in React applications. This is intended to be the primary API surface for the focus-navigation
workspace.
Re-Exported¶
ReactFocusNavigation re-exports these types from FocusNavigation:
Services and Interfaces¶
- FocusNavigationService - Core service for focus navigation
- EngineInterface - Interface for Roblox engine integration
Event Types¶
- EventPhase - Event propagation phases
- Event - Event objects passed to handlers
- EventHandler - Event handler function type
- EventHandlerMap - Map of event handlers
Data Types¶
- EventData - Union type for event data
- InputEvent - Natural user input events
- EventMap - Mapping of keycodes to event names
Context¶
FocusNavigationContext¶
type FocusNavigationContext = React.Context<FocusNavigation.FocusNavigationService?>
A context object to use for providing and consuming a FocusNavigationService instance.
Use FocusNavigationContext.Provider
to include a FocusNavigationService
instance in a React tree:
local focusNav = FocusNavigationService.new(EngineInterface.CoreGui)
React.createElement(FocusNavigationContext.Provider, {
value = focusNav
}, children)
You should generally only need to consume the context-provided FocusNavigationService
via the hooks provided with this library. However, you can also use FocusNavigationContext.Consumer
or React.useContext(FocusNavigationContext)
if you need direct access to the FocusNavigationService
.
Hooks¶
useEventMap¶
function useEventMap(
eventMap: FocusNavigation.EventMap,
innerRef: React.Ref<GuiObject>?
): React.Ref<GuiObject>
Allows a React component to register an EventMap on the FocusNavigationService. Returns a ref that must be assigned to the host component that will be associated with the event map.
The useEventMap
hook can optionally handle an inner ref, to which it will forward all updates. If you need to do something else with your ref in addition to assigning an event map, provide an inner ref.
When unmounting, changing the event map, or when the ref's value changes, the hook automatically handles de-registration and re-registration of the EventMap.
Warning
If you would like to compose the refs created by useEventMap
and useEventHandlerMap
, useEventMap
's ref should be used as the innerRef
for useEventHandlerMap
rather than the other way around.
Sample Code
local EVENT_MAP: EventMap = {
[Enum.KeyCode.ButtonX] = "ToggleItemDetails",
[Enum.KeyCode.ButtonY] = "SearchInventory",
[Enum.KeyCode.ButtonR1] = "ViewNextItem",
[Enum.KeyCode.ButtonL1] = "ViewPreviousItem",
}
local TestComponent = function(props)
local eventMapRef = useEventMap(EVENT_MAP)
-- Let's assume we receive our event handlers as a map from a parent component
local eventHandlerMapRef = useEventHandlerMap(props.eventHandlers)
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = eventMapRef,
}, {
ItemInventoryContainer = React.createElement("Frame", {
Size = UDim2.fromScale(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
ref = eventHandlerMapRef,
}, {
-- Pretend there are multiple selectable elements here
}),
})
end
useEventHandlerMap¶
function useEventHandlerMap(
eventHandlerMap: FocusNavigation.EventHandlerMap,
innerRef: React.Ref<GuiObject>?
): React.Ref<GuiObject>
Allows a React component to register an EventHandlerMap on the FocusNavigationService. Returns a ref that must be assigned to the host component that will be associated with the event map.
The useEventHandlerMap
hook can optionally handle an inner ref, to which it will forward all updates. If you need to do something else with your ref in addition to assigning an event map, provide an inner ref.
When unmounting, changing the event handler map, or when the ref's value changes, the hook automatically handles de-registration and re-registration of the EventHandlerMap.
Info
Event handler callbacks MUST be created using InputHandler
s. Like all callbacks, these functions should be declared in a static or memoized location, as they will otherwise be recreated during render cycles, which can cause them to miss events or lose input state.
Sample Code
local TestComponent = function(props)
-- Event handlers should always be memoized so that event state is not lost between renders
local eventHandlerMap = React.useMemo(function()
local map = {}
map["HideItemDetails"] = InputHandlers.OnRelease(function()
props.onHideDetails()
end)
map["SearchInventory"] = InputHandlers.OnRelease(function()
props.onSearchInventory()
end)
map["ToggleMenu"] = InputHandlers.OnRelease(function()
props.onToggleMenu()
end)
return map
end, {})
local eventHandlerRef = useEventHandlerMap(eventHandlerMap)
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = eventHandlerMapRef,
}, {
-- Pretend there are multiple selectable elements here
})
end
useEventHandler¶
function useEventHandler(
eventName: string,
eventHandler: FocusNavigation.EventHandler,
phase: FocusNavigation.EventPhase?,
innerRef: React.Ref<GuiObject>?
): React.Ref<GuiObject>
Allows a React component to register an EventHandler on the FocusNavigationService. Returns a ref that must be assigned to the host component that will be associated with the event map.
The useEventHandler
hook can optionally handle an inner ref, to which it will forward all updates. If you need to do something else with your ref in addition to assigning an event map, provide an inner ref.
When unmounting, changing the provided event handler data, or when the ref's value changes, the hook automatically handles de-registration and re-registration of the EventHandlerMap.
Info
Event handler callbacks MUST be created using InputHandler
s. Like all callbacks, these functions should be declared in a static or memoized location, as they will otherwise be recreated during render cycles, which can cause them to miss events or lose input state.
Sample Code
local TestComponent = function(props)
-- The event handler should always be memoized so that event state is not lost between renders
local eventHandler = React.useMemo(function()
return InputHandlers.OnRelease(function()
props.onCloseMenu()
end)
end, {})
local eventHandlerRef = useEventHandler("CloseMenu", eventHandler)
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = eventHandlerRef,
}, {
-- Pretend there are multiple selectable elements here
})
end
useActiveEventMap¶
function useActiveEventMap(): FocusNavigation.EventMap
Returns an EventMap that describes the current active EventMap
. The active EventMap describes all currently-bound events based on which GUI elements are focused, and what events are registered to it and its ancestors. Events bound to the same input that an ancestor is will override the ancestor's bindings to those inputs.
Sample Code
local KEYCODE_FILTER = { Enum.KeyCode.Escape, Enum.KeyCode.Tab }
local useFilteredActiveEvents = function()
local activeEventMap = useActiveEventMap()
local filteredEventMap = {}
for _, keyCode in KEYCODE_FILTER do
local eventName = activeEventMap[keyCode]
filteredEventMap[keyCode] = eventName
end
return filteredEventMap
end
useFocusedGuiObject¶
function useFocusedGuiObject(): GuiObject?
Returns the currently-focused GuiObject
via FocusNavigationService
's focusedGuiObject observable property. This hook triggers an update each time the focus changes.
See useFocusGuiObject's code sample for usage.
useFocusGuiObject¶
type FocusGuiObject = (GuiObject | nil) -> ()
function useFocusGuiObject(): FocusGuiObject
Returns a function that can be used for imperatively capturing focus. This is useful for adapting focus management to other complexities of application UI, including animations and app navigation transitions. Call this function with a GuiObject
or an object ref to move focus to the target or one of its Selectable
descendants.
You can also call this function with nil
to unfocus the UI entirely. You may want to do this in response to inputs that are not directional, like touch or mouse input.
Sample Code
local UserInputService = game:GetService("UserInputService")
local React = require(Packages.React)
function useToggleFocusOnTap(object)
local focusGuiObject = useFocusGuiObject()
local currentlyFocused = useFocusedGuiObject()
React.useEffect(function()
local tapConnection = UserInputService.TouchTap:Connect(function(position, gameProcessedEvent)
if currentlyFocused == nil then
focusGuiObject(object)
else
focusGuiObject(nil)
end
end)
return function()
tapConnection:Disconnect()
end
end, {})
end
useContainerFocusBehavior¶
function useContainerFocusBehavior(behavior: ContainerFocusBehavior, innerRef: React.Ref?): React.Ref
A hook responsible for providing the desired behavior for redirecting focus within a container. There are two general scenarios in which the assigned focus behavior will redirect focus:
- The container to which it is bound gains focus for the first time
- The container to which it is bound regains focus after navigating away and back
For more information on these behaviors and what is defined as "containers", see FocusBehaviors.
useDefaultFocusBehavior¶
function useDefaultFocusBehavior(): (defaultRef: React.Ref<Instance?>, containerRef: React.Ref<Instance?>)
Returns a defaultRef
which can be assigned to the GuiObject
that focus should default to when selection enters the container, which is similarly defined by assigning containerRef
to another GuiObject
. The element which defaultRef
is assigned to should be a descendant of the element which containerRef
is assigned to.
See default from FocusBehavior.
Sample Code
function DefaultTestComponent(props)
local defaultRef, containerRef = useDefaultFocusBehavior()
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = containerRef,
}, {
DecoyButton = React.createElement("TextButton", {
Text = "I will NOT be selected when selection enters this frame!",
}),
DefaultButton = React.createElement("TextButton", {
Text = "I will be selected when selection enters this frame!",
ref = defaultRef,
}),
})
end
useMostRecentFocusBehavior¶
function useMostRecentFocusBehavior(): containerRef: React.Ref<Instance?>
Returns a containerRef
to be assigned to the GuiObject
that will redirect focus to its most recently focused descendant. This has no effect the first time focus enters the container.
See mostRecent from FocusBehavior.
Sample Code
function MostRecentTestComponent(props)
local containerRef = useMostRecentFocusBehavior()
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = containerRef,
}, {
Button1 = React.createElement("TextButton", {
Text = "",
}),
Button2 = React.createElement("TextButton", {
Text = "",
}),
Button2 = React.createElement("TextButton", {
Text = "",
}),
})
end
useMostRecentOrDefaultFocusBehavior¶
function useMostRecentOrDefaultFocusBehavior(): (defaultRef: React.Ref<Instance?>, containerRef: React.Ref<Instance?>)
Composes the above two behaviors from useDefaultFocusBehavior
and useMostRecentFocusBehavior
, such that previous selection is restored but there is a default to fall back on.
As with useDefaultFocusBehavior
, the element which defaultRef
is assigned to should be a descendant of the element which containerRef
is assigned to.
If a valid last-focused descendant exists when refocusing, it will be redirected to, using isValidFocusTarget
to determine validity. If no valid targets are found, the default will be used.
See mostRecentOrDefault from FocusBehavior.
Sample Code
function MostRecentTestComponent(props)
local defaultRef, containerRef = useMostRecentOrDefaultFocusBehavior()
return React.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
ref = containerRef,
}, {
Button1 = React.createElement("TextButton", {
Text = "",
ref = defaultRef,
}),
Button2 = React.createElement("TextButton", {
Text = "",
}),
Button2 = React.createElement("TextButton", {
Text = "",
}),
})
end
useDispatchSyntheticEvent¶
function useDispatchSyntheticEvent(): SyntheticEventDispatcher
type SyntheticEventDispatcher = (
eventName: string,
target: GuiObject,
customEventData: SyntheticEventData?
) -> boolean
Returns a function that can programmatically dispatch synthetic events on GuiObjects. Synthetic events are artificial events created in code that behave like user-triggered events but can bypass normal input timing logic for immediate execution.
The returned dispatcher function takes an eventName
(which must be registered in the target's event map), a target
GuiObject, and optional customEventData
. It returns true
if the event was successfully dispatched, or false
if the event could not be dispatched (e.g., no event handler registered, no FocusNavigationService available, or invalid target).
This hook is particularly useful for implementing programmatic shortcuts, tappable shortcut icons, or other UI elements that need to trigger focus navigation events immediately without waiting for user input.
Note: The target GuiObject must have the specified event registered in its event map via registerEventMap()
and must have an event handler registered via registerEventHandler()
for the dispatch to succeed.
Sample Code
local function ShortcutButton(props)
local dispatch = useDispatchSyntheticEvent()
local buttonRef = React.useRef(nil)
-- Register the button with a "quick_action" event
React.useEffect(function()
if buttonRef.current then
local focusNavigationService = React.useContext(FocusNavigationContext)
if focusNavigationService then
-- Register event map: ButtonA key triggers "quick_action"
focusNavigationService:registerEventMap(buttonRef.current, {
[Enum.KeyCode.ButtonA] = "quick_action",
})
-- Register event handler for "quick_action"
focusNavigationService:registerEventHandler(buttonRef.current, "quick_action", function(event)
print("Quick action triggered!", event.synthetic and "synthetic" or "natural")
if props.onQuickAction then
props.onQuickAction()
end
end)
end
end
end, {})
local handleClick = React.useCallback(function()
if buttonRef.current then
-- Dispatch synthetic event with immediate execution
local success = dispatch("quick_action", buttonRef.current, {
immediateDispatch = true,
})
if not success then
warn("Failed to dispatch quick_action event")
end
end
end, { dispatch })
return React.createElement("TextButton", {
Text = "Quick Action Shortcut",
ref = buttonRef,
Size = UDim2.fromOffset(200, 50),
[React.Event.Activated] = handleClick,
})
end