Skip to content

Drag Listeners

Dragging in SceneryStack follows the model-view separation pattern: the listener writes to a model positionProperty (in model coordinates), and the node's position updates through the normal observer wiring — the listener never moves the node directly.

DragListener (pointer input)

ts
import { DragListener } from 'scenerystack/scenery';

bodyNode.addInputListener( new DragListener( {

  // The model Property to write, in MODEL coordinates...
  positionProperty: body.positionProperty,

  // ...which works because the listener converts through this transform.
  transform: modelViewTransform,

  // Constrain dragging to a region, in model coordinates.
  dragBoundsProperty: model.dragBoundsProperty,

  start: () => body.userControlledProperty.set( true ),
  end: () => body.userControlledProperty.set( false )
} ) );
bodyNode.cursor = 'pointer';

Key options:

OptionEffect
positionPropertyProperty written during the drag (model coordinates when transform is provided)
transformA ModelViewTransform2; converts pointer motion into model units
dragBoundsPropertyClamps the position to bounds (same frame as positionProperty)
start / drag / endCallbacks around the drag lifecycle
useParentOffsetKeeps the grab point fixed relative to the node instead of jumping its origin to the pointer
allowTouchSnagLets a touch that starts on the node immediately begin dragging (default true)
targetNodeNode whose coordinate frame is used, when the listener is attached elsewhere

Setting userControlledProperty in start/end is the standard way to tell the model to stop animating a body while the user holds it.

KeyboardDragListener (alternative input)

Pointer dragging alone is not accessible. KeyboardDragListener moves the same Property with arrow keys and WASD, and requires the node to be focusable in the PDOM:

ts
import { KeyboardDragListener } from 'scenerystack/scenery';

bodyNode.addInputListener( new KeyboardDragListener( {
  positionProperty: body.positionProperty,
  transform: modelViewTransform,
  dragBoundsProperty: model.dragBoundsProperty,
  dragSpeed: 150,       // model units per second while a key is held
  shiftDragSpeed: 50    // finer motion with shift held
} ) );

// PDOM: focusable with a meaningful accessible name
bodyNode.tagName = 'div';
bodyNode.focusable = true;
bodyNode.accessibleName = 'Planet';

RichDragListener (both at once)

RichDragListener bundles a DragListener and a KeyboardDragListener behind one API, so every draggable gets pointer and keyboard support from a single declaration. Prefer it for new code:

ts
import { RichDragListener } from 'scenerystack/scenery';

bodyNode.addInputListener( new RichDragListener( {
  positionProperty: body.positionProperty,
  transform: modelViewTransform,
  dragBoundsProperty: model.dragBoundsProperty,
  keyboardDragListenerOptions: {
    dragSpeed: 150
  }
} ) );

Checklist for a well-behaved draggable

  1. Listener writes a model Property through transform — the node observes it back.
  2. dragBoundsProperty keeps the object reachable (nothing draggable off-screen).
  3. Keyboard path exists (RichDragListener, or KeyboardDragListener + focusable PDOM).
  4. cursor = 'pointer' and, for touch, adequate touchArea / mouseArea dilation.
  5. userControlledProperty toggled in start/end if the model animates the object.