Skip to content

Keyboard Focus & Navigation ​

RTXUI applications are fully keyboard-operable out of the box. This page describes what users can do with the keyboard, and how to control which elements participate.

Built-in keys ​

KeyBehavior
Tab / Shift-TabMove focus through focusable elements in order.
Arrow keys / h j k lSpatial navigation: focus the nearest focusable element in that direction; inside a scrollable, scroll.
Enter / SpaceActivate the focused element (equivalent to a click).
PageUp / PageDownScroll the focused scrollable by a viewport.
Home / EndJump the focused scrollable to its start or end.
EscapeClose the open <dialog>; otherwise exit the application.

Component OnEvent overrides see events before these built-in behaviors, so custom key handling always wins.

Focus Control ​

To make any layout container (like a custom card or wrapper <div>) interactive and focusable, use these properties:

tabindex ​

Determines where the element fits in the sequential keyboard Tab navigation loop:

  • tabindex="0": The element is sequentially focusable (standard behavior).
  • tabindex="-1": The element is focusable programmatically or via mouse clicks, but skipped when hitting Tab/Shift-Tab.
  • tabindex="1" (or higher): Establishes custom sequential traversal order (lower positive values focus first).
html
<!-- Tab navigates to this div first, then the next -->
<div tabindex="1">First focusable zone</div>
<div tabindex="2">Second focusable zone</div>

focusable Shorthand ​

Set focusable="true" as a clean shorthand for setting tabindex="0":

html
<div focusable="true" class="button">
  Interactive Box
</div>

Focusable items automatically capture keyboard events (ArrowLeft, Enter, etc.) and focus CSS pseudoclasses like :focus.

autofocus ​

An element that appears carrying autofocus takes the focus, as in HTML. Put it on the field a panel or dialog opens with, inside the <if> that shows it, so that it appears, and takes the focus, each time the panel opens:

html
<if condition="{searching}">
  <input value="{query}" autofocus=""/>
</if>

Templates are XML, so the attribute needs a value; any value works. An element that is always there takes the focus only once, when the application starts. The command palette recipe uses it.

To show which element has focus without shifting the layout, give it an outline: unlike a border, it takes no room, so nothing moves when focus does.

css
button:focus { outline: round rgb(250, 204, 21); }

Live Demo ​

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Controlling focus order with tabindex.
#include <rtxui/rtxui.hpp>
#include <string_view>

using namespace rtxui;

class TabIndexDemo : public Component<TabIndexDemo> {
 public:
  std::string_view view = R"html(
      <div class="card">
        <h1>tabindex</h1>
        <p>Tab visits positive tabindex values first, in order, then 0 in document order. -1 is focusable by click only.</p>

        <div class="item" tabindex="2"><span class="tag">tabindex="2"</span>second</div>
        <div class="item" tabindex="0"><span class="tag">tabindex="0"</span>third</div>
        <div class="item" tabindex="1"><span class="tag">tabindex="1"</span>first</div>
        <div class="item" tabindex="-1"><span class="tag">tabindex="-1"</span>skipped by Tab</div>
        <div class="item" focusable="true"><span class="tag">focusable="true"</span>fourth, like tabindex="0"</div>
      </div>

      <style>
        self {
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 60;
        }
        h1 {
          color: var(--accent);
        }
        p {
          color: var(--muted);
        }
        .item {
          padding: 0 1;
        }
        .item:hover {
          background-color: rgb(33, 38, 45);
        }
        .item:focus {
          background-color: rgb(22, 45, 77);
          color: var(--accent);
        }
        .tag {
          display: inline-block;
          width: 20;
          color: var(--muted);
        }
      </style>
    )html";
};

int main() {
  auto app = Ref<TabIndexDemo>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}

Spatial Navigation ​

In addition to sequential tab navigation (using Tab/Shift-Tab), RTXUI supports Spatial Navigation. By pressing the Arrow keys (or hjkl keys), RTXUI performs a geometric search in 2D screen space to find the closest focusable element in that direction and moves focus there. This provides a natural, game-like, or TV-like focus navigation experience for complex layouts.

Live Demo - Spatial Navigation ​

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Arrow-key spatial navigation.
//
// Focus moves to the nearest focusable element in the direction pressed,
// computed from the laid-out geometry.
//
// Try it: move around the grid with the arrow keys.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>

using namespace rtxui;

class SpatialNavDemo : public Component<SpatialNavDemo> {
 public:
  std::string picked = "nothing yet";

  void Pick(std::string name) { picked = name; }

  std::string_view view = R"html(
      <div class="card">
        <h1>Spatial navigation</h1>
        <p>The arrow keys (or hjkl) move focus to the nearest element in that direction, by where things are drawn. Enter or Space activates.</p>

        <div class="grid">
          <div class="tile" tabindex="0" onclick="Pick(Files)">Files</div>
          <div class="tile" tabindex="0" onclick="Pick(Search)">Search</div>
          <div class="tile" tabindex="0" onclick="Pick(Git)">Git</div>
          <div class="tile" tabindex="0" onclick="Pick(Run)">Run</div>
          <div class="tile" tabindex="0" onclick="Pick(Debug)">Debug</div>
          <div class="tile" tabindex="0" onclick="Pick(Tests)">Tests</div>
          <div class="tile" tabindex="0" onclick="Pick(Docs)">Docs</div>
          <div class="tile" tabindex="0" onclick="Pick(Shell)">Shell</div>
          <div class="tile" tabindex="0" onclick="Pick(Settings)">Settings</div>
        </div>

        <div class="controls">
          <checkbox>Wrap</checkbox>
          <slider value="40" width="16" />
          <button onclick="Pick(Apply)">Apply</button>
        </div>

        <div class="readout">
          <span>picked</span>
          <span class="value">{picked}</span>
        </div>
      </div>

      <style>
        self {
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 54;
        }
        h1 {
          color: var(--accent);
        }
        p {
          color: var(--muted);
        }
        .grid {
          display: grid;
          grid-template-columns: repeat(3, 1fr);
          gap: 1 2;
        }
        .tile {
          border: tall;
          border-color: var(--border);
          background-color: rgb(13, 17, 23);
          text-align: center;
          transition: border-color 0.1s linear;
        }
        .tile:hover {
          background-color: rgb(33, 38, 45);
        }
        .tile:focus {
          border-color: var(--accent);
          color: var(--accent);
        }
        .controls {
          display: flex;
          justify-content: space-between;
          margin-top: 1;
        }
        checkbox:focus, slider:focus, button:focus {
          color: var(--accent);
        }
        .readout {
          display: flex;
          justify-content: space-between;
          border-top: solid;
          border-color: var(--border);
          margin-top: 1;
          padding-top: 1;
          color: var(--muted);
        }
        .value {
          color: var(--accent);
          font-weight: bold;
        }
      </style>
    )html";

  SpatialNavDemo() {
    Bind(picked);
    Bind(Pick);
  }
};

int main() {
  auto component = Ref<SpatialNavDemo>::New();
  Screen screen(component);
  screen.Loop();
  return 0;
}