Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

keys

Box(keys: [Left, Right, Space], onKeyDown: Move) — declare a key surface, then read Keyboard.Down(Left)

`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.

stable3 examples compiled by CIuieventskeyboardaccessibility

Summary#

A surface that responds to the keyboard declares which keys it owns, and then reads them:

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#

// 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#

Held STATE is the primitive; the events are derived#

Two different surfaces ask two different questions about the keyboard, and only one ordering answers both:

the surfacethe questionwhat answers it
a grid, a list, a menuwas an arrow pressed?onKeyDown
anything that movesis 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#

Two reads are two independent reads:

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#

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#

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, 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#

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 AZ · the digits Digit0Digit9 · 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#

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#

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:

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#

A key surface can take keys only while it has focuskeys: 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.

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#

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})");
    }
  }
}
component Thruster() {
  render {
    Box(keys: [Space], p: 4) {
      Text(Keyboard.Down(Space) ? "burning" : "idle");
    }
  }
}
component Strafe() {
  render {
    Box(keys: [W, A, S, D], p: 4) {
      Text(Keyboard.Down(W) && Keyboard.Down(A) ? "forward-left" : "idle");
    }
  }
}

See also#

  • onEnter — Enter as the keyboard peer of a click, on a focused field
  • onEscape — Escape as dismissal, listening page-wide rather than on a focused element
  • component — state, actions, and the render block these examples are written in
  • layout primitivesLayout.AtLeast, the other primitive that answers a question about the live page

Related

onEnter

`onEnter` runs an action when the Enter key is pressed while an element is focused — the keyboard peer of `onClick`…

onEscape

`onEscape` runs an action when the Escape key is pressed while the element is on screen. Unlike `onEnter` it is not…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

layout primitives

The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a…