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

Reference / UI

onEscape

Box(onEscape: Close) — run an action when Escape is pressed, wherever focus happens to be

`onEscape` runs an action when the Escape key is pressed while the element is on screen. Unlike `onEnter` it is not scoped to the focused element — the thing being dismissed is open, not focused — so it listens page-wide, and only the innermost open surface responds to a given keystroke.

stable1 example compiled by CIuieventskeyboardoverlays

Summary#

onEscape binds an action to the Escape key, for the universal "get me out of this" gesture:

component Dropdown() {
  bool open = false;
  action Close() { open = false; }
  render {
    Pressable(onClick: () => open = true) { Text("Options"); }
    if (open) {
      Box(onEscape: Close) {
        Text("…menu…");
      }
    }
  }
}

Escape closes the menu whether or not anything inside it has focus.

Signature#

onEscape: <action>

Description#

It is NOT onEnter's mirror image, and that is the whole design#

The two look like a pair and behave differently, because they answer different questions:

what it acts onso it listens
onEnterthe thing you are on — confirm this fieldon the focused element
onEscapethe thing that is open — dismiss this overlaypage-wide, while mounted

A popover, a drawer, a lightbox, a dropdown — almost none of them hold focus, because the pointer opened them. Scoped to the element, onEscape would compile, wire up correctly, and then never fire for the single case it exists to serve. So it listens for as long as the element is on screen, and stops the moment it leaves.

Nested overlays close one layer per keystroke#

Only the innermost live handler runs. A dialog opened over a drawer closes the dialog; pressing Escape again closes the drawer. Every overlay on the page does not collapse at once, which is what a page-wide listener would do if each one answered independently.

It is not blocked by a busy control#

Deliberately, onEscape is not treated as an activation: it has no in-flight guard and no busy spinner. Dismissing is local — an overlay you cannot close because something else on it is still loading is a worse failure than a double dismissal, which does nothing anyway.

An IME keystroke is not yours#

Escape also cancels an input-method candidate list. That keystroke belongs to the IME, so it does not run your action.

Examples#

component Shell() {
  bool drawer = false;
  action Open()  { drawer = true; }
  action Close() { drawer = false; }
  render {
    Pressable(onClick: Open) { Text("Menu"); }
    if (drawer) {
      Box(onEscape: Close, p: 4) {
        Pressable(onClick: Close) { Text("Close"); }
      }
    }
  }
}

Note that the button and the key run the same action. That is the pattern to keep: a dismissal reachable only by pointer is unreachable for anyone not using one, and two separate code paths drift.

See also#

  • onEnter — Enter as the keyboard peer of a click, scoped to the focused element
  • keys — declaring a key surface, and reading whether a key is held right now
  • component — state, actions, and the render block these examples are written in

Related

onEnter

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

keys

`keys:` declares that an element owns a set of keys: it becomes focusable, those keys stop scrolling the page, and…

component

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