Observer & Gesture

Respond to pointer, touch, wheel, and scroll gestures with 21 callback events.

.onGesture() turns any timeline into a gesture-driven animation. It listens to pointer, touch, wheel, and scroll inputs and fires one of 21 callback events — letting you map gestures to animation actions like play, reverse, progressUp, or playNext.

Basic Usage

typescript
import { Motion } from "@motion.page/sdk";

Motion("swipe-panel", ".panel", {
  to: { x: "-100%" },
  duration: 0.4,
  ease: "power2.inOut",
}).onGesture({
  types: ["pointer", "touch"],
  events: {
    LeftComplete: "play",
    RightComplete: "reverse",
  },
});

Options

OptionTypeDefaultDescription
typesGestureInputType[]Required. One or more input types to observe: 'pointer', 'touch', 'wheel', 'scroll'
eventsPartial<Record<GestureEvent, GestureAction>>Required. Map of event names to animation actions
targetstring | ElementwindowElement to observe. Defaults to window for global gestures. Use a selector for element-scoped gestures
tolerancenumber1Minimum distance in pixels before a gesture is recognized
dragMinimumnumber10Minimum drag distance before drag events fire. Applies to pointer and touch types
wheelSpeednumber1Sensitivity multiplier for wheel events
scrollSpeednumber1Sensitivity multiplier for scroll events
stopDelaynumber150Milliseconds of inactivity before the Stop event fires
smoothnumber0Smoothing factor 0–1. Higher values add lag to progress-based actions
animationStepnumber | Partial<Record<GestureEvent, number>>0.1How much of the timeline to step per gesture (0.1 = 10%). Pass an object to set per-event steps
preventDefaultbooleanfalseCall event.preventDefault() on observed events. Makes listeners non-passive — required for blocking native scroll on wheel/touch
lockAxisbooleanfalseLock gesture detection to the dominant axis, preventing diagonal gestures from firing both axes
eachbooleanfalseCreate an independent timeline instance per matched element

stopDelay is in milliseconds, not seconds. The default 150 means 150ms of inactivity triggers Stop. This is unlike other time values in the SDK which use seconds.


Gesture Types

TypeDetectsUse for
'pointer'Mouse movement and clicksDesktop swipe detection, drag interactions, hover-based gestures
'touch'Touchscreen swipes and tapsMobile swipe navigation, touch drag interfaces
'wheel'Mouse wheel / trackpad scrollWheel-driven animation progress, custom scrolljacking
'scroll'Native page scroll directionDirection-aware scroll reactions (distinct from position-based ScrollTrigger)

You can combine multiple types in one trigger:

typescript
// Respond to both touch and pointer with the same callbacks
.onGesture({
  types: ["pointer", "touch"],
  events: { Up: "play", Down: "reverse" },
})

Event Reference

Events are the keys in the events map. They describe what gesture was detected and when within the gesture lifecycle.

Direction Events

These fire continuously while the gesture moves in that direction.

EventFires when
UpGesture is actively moving upward
DownGesture is actively moving downward
LeftGesture is actively moving left
RightGesture is actively moving right

Direction Complete Events

These fire once on release if that direction was the dominant movement.

EventFires when
UpCompleteGesture ended while moving upward
DownCompleteGesture ended while moving downward
LeftCompleteGesture ended while moving left
RightCompleteGesture ended while moving right

Change Events

EventFires when
ChangeAny gesture movement occurs (any direction)
ChangeXHorizontal movement detected
ChangeYVertical movement detected

Toggle Events

Toggle events fire once per direction change — when the gesture switches from moving in one direction to another.

EventFires when
ToggleXGesture changes horizontal direction (left ↔ right)
ToggleYGesture changes vertical direction (up ↔ down)

Press & Drag Events

EventFires when
PressInitImmediately on press, before start position is recorded. No delta available yet
PressAfter start position is recorded (delta is available)
ReleaseOn pointer/touch release
DragDuring active drag movement (after dragMinimum threshold is exceeded)
DragEndWhen drag movement ends

Stop & Hover Events

EventFires when
StopOnce after stopDelay ms of inactivity. Useful for “idle” state resets
HoverPointer enters the target element. Requires a target element — does nothing on window
HoverEndPointer leaves the target element. Requires a target element — does nothing on window

Action Reference

Actions are the values in the events map. They control what the timeline does when that event fires.

ActionDescription
'play'Play the timeline forward
'pause'Pause playback at current position
'reverse'Play the timeline backward
'restart'Seek to start and play forward
'toggle'Toggle between play and pause
'reset'Jump instantly to the start without playing
'complete'Jump instantly to the end without playing
'kill'Stop and remove the timeline
'playReverse'Smart alternate: if at start, play forward; if at end, reverse
'progressUp'Step the timeline forward by animationStep
'progressDown'Step the timeline backward by animationStep
'playNext'Play the next element’s timeline. Requires each: true
'playPrevious'Play the previous element’s timeline. Requires each: true

Observer vs. ScrollTrigger

Both .onGesture() and .onScroll() can react to scrolling, but they work very differently:

.onGesture() with 'scroll'.onScroll()
Based onScroll directionScroll position
FiresWhen user scrolls up or downWhen element reaches a scroll position
Typical useNavigation, slide transitions, direction-aware effectsReveal animations, parallax, scrubbing
Progress tied to scroll?No (unless using progressUp/progressDown)Yes (with scrub: true)
Works with non-scroll inputs?Yes (pointer, touch, wheel)No

Use .onGesture() when you care about which way the user is moving. Use Scroll Trigger when you care about where they’ve scrolled to.


Swipe Detection

Detect completed swipe gestures using the *Complete events. These fire once per swipe when the finger or pointer lifts — not continuously during movement.

swipe-nav.ts
import { Motion } from "@motion.page/sdk";

// Slide a panel in/out with swipe gestures
Motion("drawer", ".drawer", {
from: { x: "100%" },
duration: 0.4,
ease: "power2.inOut",
}).onGesture({
target: ".drawer-area",
types: ["pointer", "touch"],
events: {
  LeftComplete: "play",    // Swipe left → open drawer
  RightComplete: "reverse", // Swipe right → close drawer
},
tolerance: 20,       // Require 20px movement before recognizing gesture
dragMinimum: 30,     // Require 30px drag before firing drag events
preventDefault: true, // Block default touch scroll during gesture
});

Wheel-Driven Animation Progress

Use progressUp / progressDown with the 'wheel' type to scrub a timeline with the mouse wheel — without locking the page scroll position the way onScroll with scrub does.

wheel-progress.ts
import { Motion } from "@motion.page/sdk";

// Step through a multi-frame animation with the wheel
Motion("frames", ".illustration", {
to: { backgroundPosition: "0% 100%" },
duration: 1,
}).onGesture({
types: ["wheel"],
events: {
  Up: "progressUp",
  Down: "progressDown",
},
animationStep: 0.05, // Advance 5% of timeline per wheel tick
wheelSpeed: 1.5,     // Slightly more sensitive
});

Per-event step sizes let you make one direction faster than the other:

typescript
.onGesture({
  types: ["wheel"],
  events: { Up: "progressUp", Down: "progressDown" },
  animationStep: { Up: 0.15, Down: 0.05 },
})

Touch Swipe with Live Demo


playNext and playPrevious advance or retreat through a set of per-element timelines. They require each: true — the gesture drives each element’s own timeline in sequence.

gallery-swipe.ts
import { Motion } from "@motion.page/sdk";

// Each slide has its own timeline; gestures step through them in order
Motion("slides", ".slide", {
from: { opacity: 0, x: 80 },
duration: 0.5,
ease: "power2.out",
}).onGesture({
types: ["pointer", "touch", "wheel"],
each: true,
events: {
  LeftComplete: "playNext",
  RightComplete: "playPrevious",
  Up: "progressUp",
  Down: "progressDown",
},
animationStep: 0.08,
lockAxis: true, // Prevent diagonal gesture ambiguity
});

Scroll Direction Reactions

Use the 'scroll' type to fire animations based on the direction the user scrolls — not their scroll position. A common pattern: hide a navbar on scroll down, reveal it on scroll up.

scroll-direction.ts
import { Motion } from "@motion.page/sdk";

// Hide navbar on scroll down, reveal on scroll up
Motion("navbar", ".navbar", {
to: { y: "-100%", opacity: 0 },
duration: 0.35,
ease: "power2.inOut",
}).onGesture({
types: ["scroll"],
events: {
  Down: "play",      // Scrolling down → hide
  Up: "reverse",     // Scrolling up → reveal
},
tolerance: 5,
smooth: 0.1,
});

Stop Event — Idle Reset

The Stop event fires once after stopDelay milliseconds of inactivity. Use it to reset the animation when the user stops interacting.

typescript
Motion("progress-bar", ".bar", {
  to: { scaleX: 1 },
  duration: 1,
  ease: "none",
}).onGesture({
  types: ["wheel"],
  events: {
    Up: "progressUp",
    Down: "progressDown",
    Stop: "reverse",   // Rewind bar when wheel stops
  },
  stopDelay: 800,      // Wait 800ms of stillness before firing Stop
  animationStep: 0.04,
});

Hover Gestures on Elements

Hover and HoverEnd fire when the pointer enters and leaves the target element. They silently do nothing if target is window.

typescript
Motion("card-glow", ".card", {
  to: { boxShadow: "0 0 32px rgba(102, 51, 238, 0.6)", y: -6 },
  duration: 0.3,
  ease: "power2.out",
}).onGesture({
  target: ".card",
  types: ["pointer"],
  events: {
    Hover: "play",
    HoverEnd: "reverse",
  },
});

For hover effects this simple, .onHover() is a cleaner choice. Use gesture Hover/HoverEnd events when you need them alongside other gesture callbacks in one config.


each — Independent Per-Element Instances

Without each, all matched elements share one timeline. With each: true, every element gets its own independent instance — essential for gallery navigation with playNext/playPrevious.

typescript
// Without each — all cards animate together on any swipe
Motion("cards", ".card", {
  from: { opacity: 0, x: 60 },
  duration: 0.5,
}).onGesture({
  types: ["touch"],
  events: { LeftComplete: "play" },
});

// With each — every card has its own state
Motion("cards", ".card", {
  from: { opacity: 0, x: 60 },
  duration: 0.5,
}).onGesture({
  each: true,
  types: ["touch"],
  events: {
    LeftComplete: "playNext",
    RightComplete: "playPrevious",
  },
});

Tips & Gotchas

stopDelay is in milliseconds. The default 150 means 150ms. Do not pass 1.5 expecting 1.5 seconds — pass 1500.

Hover / HoverEnd require a target element. They silently do nothing when target is omitted (which defaults to window). Always provide a target selector for hover events.

preventDefault: true makes listeners non-passive. Required to block native browser scroll when using wheel or touch types. Without it, calling event.preventDefault() would throw a warning in modern browsers.

lockAxis: true prevents diagonal gestures. The first axis of movement is locked in. Useful for sliders and galleries where you want clean horizontal or vertical gestures only.

playNext / playPrevious only work with each: true. Without per-element timelines, there is no concept of “next” or “previous” to step through.