> ## Documentation Index
> Fetch the complete documentation index at: https://ej3mplo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Controller and keyboard

> Spatial navigation with the kit, and the hint bar.

ejGames turns the keyboard and the controller into **actions**, the same for both:

| Action | Keyboard | Controller |
| - | - | - |
| `up` `down` `left` `right` | arrows | d-pad or left stick |
| `accept` | Enter, Space | A (✕ on PlayStation) |
| `back` | Esc, Backspace | B (○) |
| `x` / `y` | E / F | X / Y (□ / △) |
| `lb` / `rb` | Page Up / Page Down (and Shift+Tab / Tab) | LB / RB |
| `lt` / `rt` | Home / End | LT / RT |
| `menu` | the context menu key | Start / Menu |
| `view` | — | Back / View |

The controller's Guide button always opens ejGames' quick menu. F5, F11, Ctrl + F,
Ctrl + , , Ctrl + P and Ctrl + K are launcher shortcuts and do not reach your
theme as actions.

You do not have to work out what is above or to the right. The kit has
**spatial navigation** in `focus.js`.

1. Mark everything that can be chosen with **`data-focus`**. The two buttons in
   the bar already have it in the HTML. Add it to the card and to the empty
   library button:

   ```js theme={null}
   { class: "tarjeta", "data-focus": "", "data-id": g.id, "data-firma": firma, title: g.title, onclick: () => jugar(g.id) },
   // …
   h("button", { class: "boton", "data-focus": "", onclick: () => ejg.ui.open("add-folder") }, "Añadir juegos")
   ```

2. Create the focus and connect it to the actions. Add this at the top of `main.js`:

   ```js theme={null}
   import { createFocus, bindNav } from "/_sdk/kit/focus.js";
   import { hints } from "/_sdk/kit/hints.js";
   ```

   and, after `ready()`:

   ```js theme={null}
   const foco = createFocus();

   bindNav(foco, {
     y: () => {
       const id = Number(foco.current?.dataset.id);
       if (!id) return false;
       ejg.game.favorite(id);
     },
     menu: () => {
       ejg.ui.open("menu");
     },
   });

   // When a launcher screen closes (settings, search…), take the focus back.
   ejg.on("focus-return", () => foco.restore());

   // The bar buttons open launcher screens.
   for (const b of document.querySelectorAll("[data-ui]")) {
     b.addEventListener("click", () => ejg.ui.open(b.dataset.ui));
   }
   ```

3. Show the focus in CSS. The focused element has the class **`is-focused`**:

   ```css theme={null}
   button:focus { outline: none; } /* focus is drawn with .is-focused */
   .acciones button.is-focused { color: var(--texto); background: color-mix(in srgb, var(--texto) 12%, transparent); }
   .tarjeta.is-focused .caratula { transform: scale(1.03); box-shadow: 0 0 0 3px var(--acento); }
   .tarjeta.is-focused .nombre { color: var(--texto); }
   ```

4. Make sure something is always focused. At the end of `pintar()`, after `keyed(…)`:

   ```js theme={null}
   // If the focused element is gone (at startup, or because its card was rebuilt
   // when the cover arrived), focus moves to its replacement, or to the first.
   if (!foco.current?.isConnected) {
     const otra = (idEnfocado && rejilla.querySelector(`[data-id="${idEnfocado}"]`)) || rejilla.querySelector("[data-focus]");
     if (otra) foco.focus(otra, { noScroll: true, silent: true });
   }
   ```

   with `const idEnfocado = foco.current?.dataset.id;` at the start of `pintar()`.

How it works:

* **`bindNav(focus, { action: function })`**: the arrows move focus and `accept`
  clicks the focused element (with its sound). Your functions run first. If one
  returns **`false`**, the action is **not** treated as handled. For `back`, that
  means ejGames handles it (it closes whatever it has open and, in Big Picture,
  opens the quick menu).
* **`foco.current`** is the focused element. `foco.focus(el)` focuses a specific
  one and `foco.first(container)` focuses the first in a container. With
  `{ silent: true }` it makes no sound, and with `{ noScroll: true }` it does not
  scroll.
* **`ejg.game.favorite(id)`** toggles the favorite (or sets it if you pass
  `true`/`false`). You do not need to repaint by hand: the change arrives through
  `library.onChange` and the card updates itself.
* **`data-focus-group`** (on `#rejilla`) remembers the last focused element of
  the group. If you go up to the bar and come back down, you return to the same
  cover.
* **With the mouse**, passing over an element focuses it, and leaving removes the
  mark, so it does not stay stuck. With a controller the cursor is hidden.

## The hint bar

`hints()` draws the controls with the symbols of the device in use: A B X Y with
an Xbox controller, ✕ ○ □ △ with a PlayStation one, Enter, Esc, E, F with a
keyboard. It changes on its own when you change device:

```js theme={null}
const pistas = hints(document.querySelector("#pistas"), [["accept", "Jugar"], ["y", "Favorito"], ["menu", "Menú"]]);
```

You can change the texts later with `pistas.set([...])`. The `<html>` element
carries `data-input="mouse|keyboard|gamepad"`, so hiding the bar with the mouse
is pure CSS:

```css theme={null}
.pistas {
  position: fixed; right: 20px; bottom: 16px; z-index: 20;
  display: flex; gap: 18px; padding: 8px 14px; border-radius: 99px;
  background: rgb(0 0 0 / 0.55); backdrop-filter: blur(8px); font-size: 13px;
}
.pistas:empty, html[data-input="mouse"] .pistas { display: none; }
```

Try it now with the arrows, Enter and F (favorite).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.