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 habitOneJS
e.offsetX, e.offsetYe.localX, e.localY
e.clientX, e.clientYe.x, e.y
e.newValuee.value
e.target.valuee.value
e.target as an elementan 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 onNavigationMove it keeps focus where it is, so a component can handle arrow keys or a gamepad stick itself.
  • In onKeyDown it keeps the key from the control underneath: a TextField does not receive a key you handle yourself, Enter included. It does not cancel the navigation event the same key press produces, so handle that in onNavigationSubmit or onNavigationMove.

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#

PropertyTypeDescription
typestringEvent type name
xnumberX position in panel coordinates
ynumberY position in panel coordinates
buttonnumberMouse button (0=left, 1=right, 2=middle)
pointerIdnumberUnique 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#

PropertyTypeDescription
keyCodenumberUnity KeyCode value (compare with CS.UnityEngine.KeyCode.*)
keystringKeyCode name, such as "Return" or "A"
charstringTyped character, empty when the key produces none
shift, ctrl, alt, metabooleanModifier keys held

Wheel Event#

PropertyTypeDescription
typestringEvent type name
deltaXnumberHorizontal scroll amount
deltaYnumberVertical scroll amount (positive = down)

Change Event#

PropertyTypeDescription
typestringEvent type name
valueTNew 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 PropUI Toolkit Event
onClickClickEvent
onPointerDownPointerDownEvent
onPointerUpPointerUpEvent
onPointerMovePointerMoveEvent
onPointerEnterPointerEnterEvent
onPointerLeavePointerLeaveEvent
onPointerCancelPointerCancelEvent
onPointerCapturePointerCaptureEvent
onPointerCaptureOutPointerCaptureOutEvent
onWheelWheelEvent

Mouse#

React PropUI Toolkit Event
onMouseDownMouseDownEvent
onMouseUpMouseUpEvent
onMouseMoveMouseMoveEvent
onMouseEnterMouseEnterEvent
onMouseLeaveMouseLeaveEvent
onMouseOverMouseOverEvent
onMouseOutMouseOutEvent

Focus & Input#

React PropUI Toolkit Event
onFocusFocusInEvent
onBlurFocusOutEvent
onFocusInFocusInEvent
onFocusOutFocusOutEvent
onChangeChangeEvent<T>
onInputInputEvent
onKeyDownKeyDownEvent
onKeyUpKeyUpEvent

Layout#

React PropUI Toolkit Event
onGeometryChangedGeometryChangedEvent
onTransitionRunTransitionRunEvent
onTransitionStartTransitionStartEvent
onTransitionEndTransitionEndEvent
onTransitionCancelTransitionCancelEvent
React PropUI Toolkit Event
onNavigationMoveNavigationMoveEvent
onNavigationSubmitNavigationSubmitEvent
onNavigationCancelNavigationCancelEvent

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.