# keys

> `keys:` declares that an element owns a set of keys: it becomes focusable, those keys stop scrolling the page, and `Keyboard.Down(Left)` answers whether one is held right now. Held state is the primitive — `onKeyDown` is the discrete convenience over it — because "is Left down?" is the question a moving surface actually asks.

<!-- id: ui-keys · area: ui · stability: stable · html: https://osysharp.com/reference/ui/keys/ -->

## Summary        {#summary}
A surface that responds to the keyboard declares which keys it **owns**, and then reads them:

```osy syntax
component Board() {
  int x = 0;
  action Move(string key) { x = key == "Left" ? x - 1 : x + 1; }
  render {
    Box(keys: [Left, Right], onKeyDown: Move) {
      Text($"position {x}");
    }
  }
}
```

Declaring `keys:` does three things at once, and they are not separable — a surface that could not be focused would
hear nothing, and one that heard arrows without claiming them would scroll the page while it moved:

1. the element becomes **focusable**, so clicking it or tabbing to it gives it the keyboard;
2. it **owns** exactly the listed keys — they no longer scroll, and every other key is left alone;
3. `Keyboard.Down(...)` and `onKeyDown`/`onKeyUp` answer for those keys while it has focus.

## Signature      {#signature}
```osy syntax
// on any element — declares the surface
keys: [<Key>, <Key>, …]

// the discrete events (both optional; each hands the action the key NAME)
onKeyDown: <action>          // action Move(string key)
onKeyUp:   <action>

// the held-state read — legal in `render` AND in a client action body
Keyboard.Down(<Key>)   // bool: is that key down right now?
```

## Description    {#description}

### Held STATE is the primitive; the events are derived   {#held-state}
Two different surfaces ask two different questions about the keyboard, and only one ordering answers both:

| the surface | the question | what answers it |
|---|---|---|
| a grid, a list, a menu | *was an arrow **pressed**?* | `onKeyDown` |
| anything that **moves** | *is Left **down right now**?* | `Keyboard.Down(Left)` |

Holding an arrow slides a piece; holding W walks. No vocabulary of fired events can express that — you would end up
tracking "which keys are currently down" in your own state, updating it from two handlers, and getting the edge cases
wrong. So the **held set is the primitive**, and `onKeyDown` is the convenient discrete half over it.

This matters most for the case that looks like it works and does not: a held key **auto-repeats**, firing `keydown`
roughly thirty times a second. `onKeyDown` runs **once per press**, on the real transition — so an app that moves one
step per press gets one step. An app that wants continuous motion reads `Keyboard.Down` on each tick instead.

### Simultaneous keys just work   {#simultaneous}
Two reads are two independent reads:

```osy title="two held keys are two independent reads — no arbitration to lose" syntax
if (Keyboard.Down(W)) { walk(); }
if (Keyboard.Down(A)) { strafe(); }
```

Both are true while both keys are down. There is no "current key" and no arbitration to lose.

### Ownership is declared, never inferred   {#ownership}
Arrows and Space scroll the page. Naming a key in `keys:` is what makes the platform claim it — and claim **only** it.
Every key you did not list passes through untouched, so <kbd>Tab</kbd> still moves focus and the browser's own
shortcuts still work. That is the difference between a key surface and a page that has taken the keyboard hostage.

### It is focus-scoped   {#focus-scope}
A key surface hears keys while it **has focus**. Two boards on one page therefore never both move, and a keystroke
meant for a text field is not swallowed by a board elsewhere on the page.

This is the opposite scoping from [onEscape](https://osysharp.com/reference/ui/on-escape/), deliberately: Escape dismisses the thing that is *open*, which is
almost never the thing that is *focused*, so it listens page-wide. A key surface is the thing you are interacting
with. (When focus leaves mid-hold — a <kbd>Cmd</kbd>+<kbd>Tab</kbd> while holding an arrow — every held key is
released, because the browser delivers that key-up to whatever has focus now, which is not you.)

### Keys are NAMES, checked when you compile   {#names}
`Left`, `Space`, `W`, `Digit1`, `Shift` — written bare, from a fixed vocabulary. A misspelling is a compile error
that suggests the nearest real key, rather than a surface that renders, takes focus, and silently never responds.

Available: the arrows `Left` `Right` `Up` `Down` · `Space` `Enter` `Escape` `Tab` `Backspace` `Delete` ·
`Home` `End` `PageUp` `PageDown` · the letters `A`–`Z` · the digits `Digit0`–`Digit9` · the modifiers `Shift`
`Control` `Alt` `Meta`.

A modifier names **either** physical key — `Keyboard.Down(Shift)` is true for the left or the right one, and
`onKeyDown` still hands you `Shift`. You are never asked to care that there are two.

### Keys are PHYSICAL POSITIONS, not the character produced   {#physical}
`Keyboard.Down(W)` means *the key where W sits*, not *the key that types "w"*. This is what makes a `WASD` movement
cluster keep its shape when someone holds <kbd>Shift</kbd> to run, and on a keyboard layout that is not QWERTY.

The cost is worth stating plainly: on an AZERTY keyboard, `W` is the key physically where W is on QWERTY, whatever is
printed on the cap. That is what movement keys mean and what players expect — but it is why this is a **gesture**
vocabulary, not a text one. To read what somebody *typed*, bind an `Input` and use `onInput`, where keyboard layout
and IME are handled properly and this question never comes up.

### Where `Keyboard.Down` may be read   {#where-read}
In a `render` block, where the read is **reactive** — a surface bound to a held key repaints on press and on release
with nothing wired by you:

```osy title="read in render, so the surface repaints on press and on release" syntax
Box(keys: [Space], bg: Keyboard.Down(Space) ? Accent : Surface) { Text("hold me"); }
```

…and in a **client action body**, where it is a point-in-time read — which is what a tick handler wants.

It is not available on the server: no keyboard is attached to one. A server-rendered page paints as though nothing is
held (because nothing is), and the first real keystroke corrects it. A server function that reads it is a compile
error naming the conflict.

## Making a key surface focusable — `autoFocus`   {#auto-focus}

A key surface can take keys only while it **has focus** — `keys:` makes it focusable (`tabindex`), and clicking it
focuses it. `autoFocus: true` says it should start out holding focus, so the first keystroke works without a click
first.

```osy syntax
Box(keys: [Left, Right, Shift], autoFocus: true) { … }
```

⚠ **Without it, a modifier-gated click does the UNMODIFIED thing on a freshly loaded page** — and silently. If a
page reads `Keyboard.Down(Shift)` inside a click action to mean "flag rather than reveal", the very first
shift-click reveals instead, then works correctly ever after. That reads as a flake rather than as a missing
declaration, which is why the prop exists.

Focusing does **not** scroll: an `autoFocus` surface below the fold will not jump the page past the heading that
explains it. And it fires on the edge — when the value becomes true — never re-asserting on an unrelated re-render,
so it cannot yank focus back from wherever the reader has tabbed to.

## Examples       {#examples}

```osy title="a focusable board that moves on arrows" test app=ui-keys
component Board() {
  int x = 0;
  int y = 0;
  action Move(string key) {
    if (key == "Left")  { x = x - 1; }
    if (key == "Right") { x = x + 1; }
    if (key == "Up")    { y = y - 1; }
    if (key == "Down")  { y = y + 1; }
  }
  render {
    Box(keys: [Left, Right, Up, Down], onKeyDown: Move, p: 4) {
      Text($"({x}, {y})");
    }
  }
}
```

```osy title="held state — the surface reacts while the key is down" test app=ui-keys-held
component Thruster() {
  render {
    Box(keys: [Space], p: 4) {
      Text(Keyboard.Down(Space) ? "burning" : "idle");
    }
  }
}
```

```osy title="two keys at once — no arbitration, just two reads" test app=ui-keys-simul
component Strafe() {
  render {
    Box(keys: [W, A, S, D], p: 4) {
      Text(Keyboard.Down(W) && Keyboard.Down(A) ? "forward-left" : "idle");
    }
  }
}
```

## See also       {#see-also}
- [onEnter](https://osysharp.com/reference/ui/on-enter/) — Enter as the keyboard peer of a click, on a focused field
- [onEscape](https://osysharp.com/reference/ui/on-escape/) — Escape as dismissal, listening page-wide rather than on a focused element
- [component](https://osysharp.com/reference/ui/component/) — state, actions, and the render block these examples are written in
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Layout.AtLeast`, the other primitive that answers a question about the live page
