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

Reference / UI

drag

Box(drag: width, dragAxis: DragAxis.Horizontal, dragStep: 8, dragMin: 60) — drag a number, keyboard included

`drag:` binds a number to a drag gesture: grabbing the element moves the value, arrow keys move the same value, and Escape puts it back where it started. You write no drag code — no begin handler, no cancel handler, no keyboard handler — which is what makes those three agree.

stable2 examples compiled by CIuieventspointerkeyboard

Summary#

A resize handle, a splitter, a slider — anything where a gesture moves a number — is one prop:

component Splitter() {
  int width = 240;
  render {
    Row {
      Box(w: width) { Text("sidebar"); }
      Box(drag: width, dragAxis: DragAxis.Horizontal, dragStep: 8, dragMin: 160, dragMax: 480) { Text("⋮"); }
      Box { Text("content"); }
    }
  }
}

That is the whole feature. Dragging the handle writes width; <kbd>←</kbd>/<kbd>→</kbd> write the same width in steps of 8; <kbd>Home</kbd>/<kbd>End</kbd> go to 160 and 480; <kbd>Esc</kbd> mid-drag puts it back where the drag started. None of that is code you write.

Signature#

drag:     <a numeric state member>     // the value the gesture moves — two-way
dragAxis: DragAxis.Horizontal | DragAxis.Vertical        // REQUIRED
dragStep: <number>                     // how far one arrow key moves it (default 1)
dragMin:  <number>                     // clamp, and where Home goes
dragMax:  <number>                     // clamp, and where End goes

Description#

It binds a VALUE, and that is why the hard parts are already right#

The obvious shape for a drag is an event — "call me with the delta". This is a binding instead, and the difference is not stylistic. Two things follow from the platform owning the value rather than the app:

Escape restores. The pre-drag value is snapshotted when you grab the handle, and Escape writes it back. With an event you would hold that start value yourself, and whether it is still right depends on when your begin handler ran, whether a re-render reset it, and what happens when two drags interleave. Cancel is the part of a drag that is always skipped and always missed; here there is nothing to skip.

The keyboard works. Arrow keys write the same bound value through the same code path. There is no second handler to write and therefore no second handler to forget, get subtly wrong, or leave behind when the drag logic changes.

You also get, without asking: the drag surviving the pointer leaving the element (so a fast drag does not drop), a movement threshold (so a click on the handle does not nudge the value), and the right touch behaviour on a phone.

dragAxis is required, and that is deliberate#

It is the one required option in the UI vocabulary. The axis decides three separate things — which direction of movement is read, which arrow keys respond, and the element's touch behaviour.

That last one is why there is no default. On a touchscreen a drag and a scroll begin identically, so the element has to declare which it is before the finger moves. A horizontal drag leaves vertical panning to the page, so the page still scrolls over your control. Get it wrong and everything looks perfect on a desktop and the page will not scroll on a phone — so the platform asks rather than guesses.

The handle is focusable#

Declaring drag: makes the element focusable, because a keyboard path you cannot reach is not a keyboard path. Click it or <kbd>Tab</kbd> to it, then use the arrows.

What it does not do#

drag: moves a number. Dragging an object — a card onto a lane, a row to a new position — is drag-and-drop, which needs to know what you picked up and what you dropped it on; that is a different vocabulary and it is not this prop with a different type.

There is also no Both axis. A drag: binds one number, and a two-axis gesture has no single value to write into it.

Examples#

component Splitter() {
  int width = 240;
  render {
    Row {
      Box(drag: width, dragAxis: DragAxis.Horizontal, dragStep: 8, dragMin: 160, dragMax: 480) {
        Text("drag me");
      }
      Text(width);
    }
  }
}
component Drawer() {
  int height = 120;
  render {
    Box(drag: height, dragAxis: DragAxis.Vertical, dragStep: 16, dragMin: 40) {
      Text(height);
    }
  }
}

See also#

  • keys — declaring a key surface, and reading whether a key is held right now
  • onEscape — Escape as dismissal elsewhere; a drag consumes its own Escape while one is in flight
  • component — state, actions, and the render block these examples are written in
  • layout primitivesRow/Stack/Box, the elements a drag: goes on

Related

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

layout primitives

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

onEscape

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