Events#
Handle user interactions with React-style event props.
Click Events#
<Button
text="Click me"
onClick={(e) => {
console.log("Clicked at", e.x, e.y)
}}
/>Pointer Events#
Track pointer (mouse/touch) interactions:
<View
style={{ width: 200, height: 200, backgroundColor: "#333" }}
onPointerDown={(e) => console.log("Down", e.x, e.y)}
onPointerUp={(e) => console.log("Up")}
onPointerMove={(e) => console.log("Move", e.x, e.y)}
onPointerEnter={(e) => console.log("Enter")}
onPointerLeave={(e) => console.log("Leave")}
>
<Label text="Hover and click me" />
</View>Focus Events#
<TextField
placeholder="Type here..."
onFocus={(e) => console.log("Focused")}
onBlur={(e) => console.log("Lost focus")}
/>onFocus and onBlur fire only on the element that gained or lost focus. onFocusIn and onFocusOut (OneJS 3.4.8+) are their bubbling counterparts, so a container hears when anything inside it gains or loses focus:
<View onFocusOut={() => console.log("Something inside lost focus")}>
<TextField placeholder="Name" />
<TextField placeholder="Email" />
</View>Focus moved from code fires these as focus moved by the user does, and as in a browser, they run before the call returns: after element.Focus(), the element's onFocus has already run.
Change Events#
<TextField value={text} onChange={(e) => setText(e.value)} />
<Toggle value={on} onChange={(e) => setOn(e.value)} label="Enable" />
<Slider value={vol} min={0} max={100} onChange={(e) => setVol(e.value)} />onChange reports what the user changed: a new value prop fires none, as in React DOM.
Coordinates and Field Names#
Two things about the event object catch out anyone arriving from the web, and neither fails loudly. A handler typed as any accepts a misspelled field and quietly hands you undefined, so the symptom is a control that does nothing rather than an error.
Pointer positions are panel coordinates. e.x and e.y are measured from the top left of the panel, not from the element your handler sits on. For a position relative to your own element, read e.localX and e.localY:
function Canvas() {
return (
<View
style={{ width: 300, height: 300 }}
onPointerDown={(e) => {
const nx = e.localX / 300 // 0 at the left edge, 1 at the right
const ny = e.localY / 300
}}
/>
)
}localX and localY are relative to currentTarget, the element whose handler is running, so on a bubbling event a parent's handler sees coordinates relative to the parent. They are computed from the element's bounds the first time a handler reads them, which costs one crossing; x and y are free.
Change events carry value. Not newValue, and not target.value.
| Web habit | OneJS |
|---|---|
e.offsetX, e.offsetY | e.localX, e.localY |
e.clientX, e.clientY | e.x, e.y |
e.newValue | e.value |
e.target.value | e.value |
e.target as an element | an integer C# handle: compare it against ref.current.__csHandle |
If you also read the pointer through the input module, keep the two apart. input.mouse.position is in Unity screen coordinates, with the origin at the bottom left and y increasing upward; event coordinates are panel coordinates, with the origin at the top left and y increasing downward. Pick one space per piece of hit testing rather than mixing them.
Keyboard Events#
<TextField
onKeyDown={(e) => {
if (e.keyCode === CS.UnityEngine.KeyCode.Return) {
console.log("Enter pressed!")
}
}}
/>Event Propagation#
Events bubble up through the component tree. Use stopPropagation() to prevent this:
<View onClick={() => console.log("Parent")}>
<Button
text="Click"
onClick={(e) => {
e.stopPropagation()
console.log("Only this handler runs")
}}
/>
</View>Suppressing native behavior#
stopPropagation() only stops OneJS's own (JavaScript-side) bubbling. To stop the native UI Toolkit behavior of a control underneath your handler (for example a ScrollView's scroll), call preventDefault():
// Keep the ScrollView from scrolling while the pointer is over this element
<ScrollView>
<View onWheel={(e) => e.preventDefault()}>
...
</View>
</ScrollView>preventDefault() works on pointer (onPointerDown/onPointerMove/onPointerUp), onClick, and onWheel handlers, on the key handlers (onKeyDown/onKeyUp, OneJS 3.4.8+), and on the navigation handlers (onNavigationMove/onNavigationSubmit/onNavigationCancel, OneJS 3.4.7+).
- In
onPointerDown(3.4.8+) it also keeps focus where it is, so pressing the element does not focus it. - In
onNavigationMoveit keeps focus where it is, so a component can handle arrow keys or a gamepad stick itself. - In
onKeyDownit keeps the key from the control underneath: aTextFielddoes not receive a key you handle yourself, Enter included. It does not cancel the navigation event the same key press produces, so handle that inonNavigationSubmitoronNavigationMove.
Before 3.4.8 a prevented pointerdown still moved focus and a prevented key still reached the control, so check any handler that already calls preventDefault() when you upgrade. Both methods are lowercase (e.preventDefault(), e.stopPropagation()). The PascalCase C# names are not available on the event object.
Pointer Capture#
Lock pointer events to a specific element. Essential for reliable dragging. Without capture, fast pointer movement can leave the element's bounds and drop events.
useExtensions(CS.UnityEngine.UIElements.PointerCaptureHelper)
function Draggable({ children }) {
const ref = useRef(null)
const [pos, setPos] = useState({ x: 0, y: 0 })
const startPos = useRef({ x: 0, y: 0 })
return (
<View
ref={ref}
style={{ position: "absolute", left: pos.x, top: pos.y }}
onPointerDown={(e) => {
ref.current.CapturePointer(e.pointerId)
startPos.current = { x: e.x - pos.x, y: e.y - pos.y }
}}
onPointerMove={(e) => {
if (ref.current.HasPointerCapture(e.pointerId)) {
setPos({
x: e.x - startPos.current.x,
y: e.y - startPos.current.y,
})
}
}}
onPointerUp={(e) => ref.current.ReleasePointer(e.pointerId)}
>
{children}
</View>
)
}useExtensions registers C# extension methods so they can be called on elements. See C# Interop: Extension Methods for details.
Other Events#
// Geometry: detect size/position changes
<View onGeometryChanged={(e) => console.log(e.newRect)} />
// Navigation: gamepad/keyboard UI navigation
<View
onNavigationMove={(e) => console.log("Direction:", e.direction)}
onNavigationSubmit={(e) => console.log("Submit")}
/>
// Mouse wheel and trackpad scroll
<View onWheel={(e) => console.log("Scroll", e.deltaY)} />
// Text as it is typed, before onChange reports the committed value
<TextField onInput={(e) => console.log("Typing", e.value)} />
// USS transitions: one event per property, as in a browser
<View className="fade" onTransitionEnd={(e) => console.log(e.propertyName, "done after", e.elapsedTime, "s")} />Mouse events (onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseOver, onMouseOut) come from the primary pointer only, so prefer the pointer events, which also cover touch and pen. Input, mouse and transition events need OneJS 3.9.8+.
Event Object Properties#
Pointer / Mouse Event#
| Property | Type | Description |
|---|---|---|
type | string | Event type name |
x | number | X position in panel coordinates |
y | number | Y position in panel coordinates |
button | number | Mouse button (0=left, 1=right, 2=middle) |
pointerId | number | Unique pointer identifier (pointer events only) |
Every synthetic event also carries target and currentTarget (C# handles, not elements) plus preventDefault(), stopPropagation(), defaultPrevented, and propagationStopped.
Key Event#
| Property | Type | Description |
|---|---|---|
keyCode | number | Unity KeyCode value (compare with CS.UnityEngine.KeyCode.*) |
key | string | KeyCode name, such as "Return" or "A" |
char | string | Typed character, empty when the key produces none |
shift, ctrl, alt, meta | boolean | Modifier keys held |
Wheel Event#
| Property | Type | Description |
|---|---|---|
type | string | Event type name |
deltaX | number | Horizontal scroll amount |
deltaY | number | Vertical scroll amount (positive = down) |
Change Event#
| Property | Type | Description |
|---|---|---|
type | string | Event type name |
value | T | New value (type depends on control) |
Other payloads: onNavigationMove carries direction ("left", "up", "right", "down", "next", "previous"), onGeometryChanged carries oldRect and newRect ({ x, y, width, height }), onInput carries value and previousValue, and the transition events carry propertyName (the USS name, such as "background-color") and elapsedTime in seconds.
Event Mapping Reference#
These are the events OneJS delivers to React props.
Click & Pointer#
| React Prop | UI Toolkit Event |
|---|---|
onClick | ClickEvent |
onPointerDown | PointerDownEvent |
onPointerUp | PointerUpEvent |
onPointerMove | PointerMoveEvent |
onPointerEnter | PointerEnterEvent |
onPointerLeave | PointerLeaveEvent |
onPointerCancel | PointerCancelEvent |
onPointerCapture | PointerCaptureEvent |
onPointerCaptureOut | PointerCaptureOutEvent |
onWheel | WheelEvent |
Mouse#
| React Prop | UI Toolkit Event |
|---|---|
onMouseDown | MouseDownEvent |
onMouseUp | MouseUpEvent |
onMouseMove | MouseMoveEvent |
onMouseEnter | MouseEnterEvent |
onMouseLeave | MouseLeaveEvent |
onMouseOver | MouseOverEvent |
onMouseOut | MouseOutEvent |
Focus & Input#
| React Prop | UI Toolkit Event |
|---|---|
onFocus | FocusInEvent |
onBlur | FocusOutEvent |
onFocusIn | FocusInEvent |
onFocusOut | FocusOutEvent |
onChange | ChangeEvent<T> |
onInput | InputEvent |
onKeyDown | KeyDownEvent |
onKeyUp | KeyUpEvent |
Layout#
| React Prop | UI Toolkit Event |
|---|---|
onGeometryChanged | GeometryChangedEvent |
onTransitionRun | TransitionRunEvent |
onTransitionStart | TransitionStartEvent |
onTransitionEnd | TransitionEndEvent |
onTransitionCancel | TransitionCancelEvent |
Navigation#
| React Prop | UI Toolkit Event |
|---|---|
onNavigationMove | NavigationMoveEvent |
onNavigationSubmit | NavigationSubmitEvent |
onNavigationCancel | NavigationCancelEvent |
Every event bubbles to ancestors exactly when UI Toolkit bubbles it: the enter and leave events, onFocus, onBlur and onGeometryChanged reach only their own element. UI Toolkit's drag, tooltip and context click events have no props, since UI Toolkit raises them only in Editor windows; use the pointer events with pointer capture for drag and drop, and onPointerDown with e.button === 1 for a right click.
Custom Rendering#
For custom drawing with onGenerateVisualContent, see the Vector Drawing guide.