Hey Developers!
We are excited to announce a series of new keybinds we are adding to the Roblox client to support keyboard navigation, out of the box, in all Experiences, with little to no additional effort required by developers !
The \ (Backslash) key will now toggle UI Selection
-
If you do not have an element selected, it selects an element in the PlayerGui.
-
If you have one selected, it will unselect it.
This is equivalent to the behavior of a controller’s Select button, and it also respects AutoSelectGuiEnabled. We’ve provided the logic for this in the following code block for reference.
local function EnableKeyboardNavigation(actionName, inputState, inputObject)
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Pass
end
-- Respect AutoSelectGuiEnabled
if not GuiService.AutoSelectGuiEnabled then
return Enum.ContextActionResult.Pass
end
-- There is a selected object, unselect it
if GuiService.SelectedObject then
GuiService.SelectedObject = nil
return Enum.ContextActionResult.Sink
end
-- Select an element in the PlayerGui
GuiService:Select(PlayerGui)
return Enum.ContextActionResult.Sink
end
ContextActionService:BindAction("EnableKeyboardUINavigation", EnableKeyboardNavigation, false, Enum.KeyCode.BackSlash)
This addition is now shown in the Controls section of the In-Game Menu as UI Selection Toggle under Misc.
In addition, PageUp, PageDown, Home, and End will trigger scrolling if your SelectedObject is a ScrollingFrame, or a descendant of one.
-
PageUp / PageDown = Up / Down
- Fn+▲ / Fn+▼ on keyboards without dedicated keys
-
Home / End = Left / Right
- Fn+◄ / Fn+► on keyboards without dedicated keys
This is equivalent to a controller’s joystick behavior when focused on a scrolling frame. The code is available below for reference.
local function ScrollSelectedElement(actionName, inputState, inputObject)
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Pass
end
local selectedObject = GuiService.SelectedObject
-- If no selected object, ignore
if selectedObject == nil then
return Enum.ContextActionResult.Pass
end
local scrollingFrame = nil
if selectedObject.ClassName == "ScrollingFrame" then
scrollingFrame = selectedObject
else
local scrollingFrameAncestor = selectedObject:FindFirstAncestorOfClass("ScrollingFrame")
if scrollingFrameAncestor ~= nil then
scrollingFrame = scrollingFrameAncestor
end
end
-- If selected object is not a scrolling frame, or a descendant of one, ignore
if scrollingFrame == nil then
return Enum.ContextActionResult.Pass
end
local scrollDistance = 0
if inputObject.KeyCode == Enum.KeyCode.PageUp or inputObject.KeyCode == Enum.KeyCode.Home then
scrollDistance = -100
elseif inputObject.KeyCode == Enum.KeyCode.PageDown or inputObject.KeyCode == Enum.KeyCode.End then
scrollDistance = 100
end
local x = scrollingFrame.CanvasPosition.X
local y = scrollingFrame.CanvasPosition.Y
if inputObject.KeyCode == Enum.KeyCode.PageUp or inputObject.KeyCode == Enum.KeyCode.PageDown then
-- Scroll vertically
y = math.max(0, math.min(y + scrollDistance, scrollingFrame.AbsoluteCanvasSize.Y))
elseif inputObject.KeyCode == Enum.KeyCode.Home or inputObject.KeyCode == Enum.KeyCode.End
then
-- Scroll horizontally
x = math.max(0, math.min(x + scrollDistance, scrollingFrame.AbsoluteCanvasSize.X))
end
scrollingFrame.CanvasPosition = Vector2.new(x, y)
return Enum.ContextActionResult.Pass
end
ContextActionService:BindAction("ScrollSelectedElement", ScrollSelectedElement, false, Enum.KeyCode.PageUp, Enum.KeyCode.PageDown, Enum.KeyCode.Home, Enum.KeyCode.End)
More Information
You will note via the code blocks that the action for scrolling always passes input, while the toggle UI selection action sinks input when an event occurs (an element is selected or deselected). This is consistent with the implementation of other keybinds we have added (e.g. emotes menu), and allows you as the developer to override this functionality if you choose to by binding an action at a higher priority with BindActionAtPriority. Events bound at the C++ level (e.g. ProximityPrompts with any of these keys as the KeyboardKey) will be completely unaffected.
Now the good stuff
Players should now be able to navigate UI in experiences using only your keyboard. Once an element is selected, use the arrow keys (▲▼◄ ►) or WASD to navigate between elements, and Enter to activate. If you find any gaps in your experience / things you can’t do, please file a bug here on the DevForum - we’ll take a look at it.
Developers - there is very little difference between supporting controllers, and supporting keyboard navigation.
-
For any shortcuts you’ve added for gamepad controllers, consider adding one for keyboard users.
- E.g. If you allow users to close modals or menus by pressing B (recommended by the Xbox Featured Experience Guidelines), allowing keyboard users to do the same thing by pressing Backspace.
- Don’t leave users guessing how to do things - show them how to perform an action either in the context of which that action is performed, or in an easily-accessible menu. For more information or best practices, visit the GetStringForKeyCode section on the UserInputService docs page.
-
Use keyboard navigation as an easy way to validate general controller navigation, through your own definitions of selection groups and the gamepad selection algorithm. Read more about this here: Gamepad UI Highlight Improvements
-
If you find your experience working well with keyboard navigation
- Consider disabling Virtual Cursor either entirely, or conditionally.
- Navigating UI with buttons is generally a more preferable experience to navigation with joysticks for users with and without disabilities - it requires less fine motor control.
- Consider enabling for Xbox if you haven’t already, and you abide by other Xbox guidelines.
- Consider disabling Virtual Cursor either entirely, or conditionally.
-
Consider improving other areas of your game for better keyboard support
- Can your ClickDetectors instead be ProximityPrompts?
If you’re currently using the Backslash key for any actions in-experience, please assess any user workflows that might be affected, consider using another key in those cases, or bind your action to a higher priority and allow users to navigate their UI with a keyboard only via other methods. We analyzed the usage of this key and it’s extremely low.There should be very little impact to experiences.
If you have your own implementation of entering / exiting keyboard navigation, you can disable the existing action by adding an action bound to the same key at a higher priority that sinks input, or set AutoSelectGuiEnabled to false
. If you do either, consider adding Backslash as a key to whatever action you define for your own keyboard navigation, or ensuring that users are otherwise aware of how to navigate your UI with keyboard navigation.
Here’s an example of how you could customize the \ key to select a specific element on the screen, rather than what GuiService:Select() chooses automatically.
local function EnableKeyboardNavigation(actionName, inputState, inputObject)
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Pass
end
-- The selected object is already active, disable
if GuiService.SelectedObject then
GuiService.SelectedObject = nil
return Enum.ContextActionResult.Sink
-- Returning Pass instead of setting the SelectedObject to nil would also
-- work, because the existing action would pass in this case as well.
end
GuiService.SelectedObject = cactusButton
return Enum.ContextActionResult.Sink
end
ContextActionService:BindActionAtPriority("CustomEnableKeyboardNavigation", EnableKeyboardNavigation, false, 100, Enum.KeyCode.BackSlash)
Please leave any suggestions or feedback in the comments. We are working to provide better guidelines on controller keyboard navigation with documentation, stay tuned .