Skip to content

Scenery Input

Scenery unifies mouse, touch, and pen input behind one event model: every physical input source becomes a Pointer (a Mouse, Touch, or Pen instance), every DOM event becomes a SceneryEvent carrying the Trail of Nodes it hit, and any Node can attach listeners that react to it — no direct DOM event handling required.

Attaching a listener

ts
import { Node, Circle } from 'scenerystack/scenery';

const ball = new Circle( 20, { fill: 'crimson', cursor: 'pointer' } );

ball.addInputListener( {
  down: event => console.log( 'pressed', event.pointer.point ),
  up: event => console.log( 'released' ),
  over: event => { ball.fill = 'orange'; },
  out: event => { ball.fill = 'crimson'; }
} );

An input listener is a plain object (or class instance) whose keys are event names. Scenery calls the matching key when that event reaches a Node the listener is attached to.

EventFires on
down / up / cancelPointer press, release, or interrupted press (any pointer type)
movePointer moved while over this Node
enter / exitPointer entered/left this Node's subtree (fires once per subtree, unlike over/out)
over / outPointer entered/left this specific Node
clickA full press-and-release on the same target
focus / blurKeyboard/PDOM focus gained or lost
keydown / keyupKey events while this Node has focus

Type-specific variants (mousedown, touchdown, pendown, ...) exist for every generic event when you need to branch on pointer type without inspecting event.pointer.type yourself.

SceneryEvent and the hit trail

The object passed to a listener is a SceneryEvent, carrying everything about this dispatch:

ts
ball.addInputListener( {
  down: event => {
    event.pointer;        // the Pointer (Mouse | Touch | Pen) that triggered this
    event.trail;           // Trail: ordered list of Nodes from the Display's root to the leaf hit
    event.target;          // the leaf-most (deepest) Node in the trail
    event.currentTarget;   // the Node this particular listener is attached to
    event.domEvent;        // the underlying raw MouseEvent/TouchEvent/PointerEvent, or null
  }
} );

Events bubble: a listener on a parent Node still receives events that were hit-tested against one of its descendants, with currentTarget reflecting which ancestor the listener is actually attached to and target/trail reflecting where the pointer actually hit. Call event.handled = true inside a listener to stop the event bubbling to ancestors, or event.abort() to stop it from reaching any further listeners at all.

Pointer areas and pickability

Two Node options control whether and where a Node can be hit at all:

ts
import { Shape } from 'scenerystack/kite';

ball.touchArea = ball.localBounds.dilated( 10 ); // easier to tap on touchscreens than the visual bounds
ball.mouseArea = ball.localBounds;               // mouse hit area, independently of touch
ball.pickable = false;                            // excluded from hit-testing (still painted, if visible)
ball.inputEnabled = false;                        // subtree stops responding to input, without changing pickable

touchArea/mouseArea accept a kite Shape or a Bounds2 and are commonly dilated beyond the visual bounds, since touch targets smaller than ~44px are hard to hit reliably.

Structured listeners: PressListener, FireListener, DragListener

Handling down/up/cancel manually gets repetitive and easy to get subtly wrong (interrupted presses, multitouch). The scenery listener classes wrap the raw events into higher-level behavior:

ts
import { FireListener, PressListener, DragListener } from 'scenerystack/scenery';

button.addInputListener( new FireListener( {
  fire: () => console.log( 'fired!' ) // press-and-release, handles interruption for you
} ) );

someNode.addInputListener( new PressListener( {
  press: event => { /* pressed down */ },
  release: event => { /* released, still over the node */ }
} ) );

DragListener (and its keyboard-accessible counterpart KeyboardDragListener) is the most common of these — see Drag Listeners for the full pattern of wiring a listener to a model positionProperty through a ModelViewTransform2.

Pointer input alone is never accessible

Every listener shown above only responds to mouse/touch/pen. Keyboard users and screen reader users interact through the Parallel DOM instead — a fully accessible custom interaction needs a tagName, focusable: true, and a keyboard-driven listener (KeyboardListener or KeyboardDragListener) in addition to any pointer listener. See the Accessible Interaction Tutorial for a worked example.

Where to go next