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

Option Type Default Description
types GestureInputType[] Required. One or more input types to observe: 'pointer', 'touch', 'wheel', 'scroll'
events Partial<Record<GestureEvent, GestureAction>> Required. Map of event names to animation actions
target string | Element window Element to observe. Defaults to window for global gestures. Use a selector for element-scoped gestures
tolerance number 1 Minimum distance in pixels before a gesture is recognized
dragMinimum number 10 Minimum drag distance before drag events fire. Applies to pointer and touch types
wheelSpeed number 1 Sensitivity multiplier for wheel events
scrollSpeed number 1 Sensitivity multiplier for scroll events
stopDelay number 150 Milliseconds of inactivity before the Stop event fires
smooth number 0 Smoothing factor 0–1. Higher values add lag to progress-based actions
animationStep number | Partial<Record<GestureEvent, number>> 0.1 How much of the timeline to step per gesture (0.1 = 10%). Pass an object to set per-event steps
preventDefault boolean false Call event.preventDefault() on observed events. Makes listeners non-passive — required for blocking native scroll on wheel/touch
lockAxis boolean false Lock gesture detection to the dominant axis, preventing diagonal gestures from firing both axes
each boolean false Create 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

Type Detects Use for
'pointer' Mouse movement and clicks Desktop swipe detection, drag interactions, hover-based gestures
'touch' Touchscreen swipes and taps Mobile swipe navigation, touch drag interfaces
'wheel' Mouse wheel / trackpad scroll Wheel-driven animation progress, custom scrolljacking
'scroll' Native page scroll direction Direction-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.

Event Fires when
Up Gesture is actively moving upward
Down Gesture is actively moving downward
Left Gesture is actively moving left
Right Gesture is actively moving right

Direction Complete Events

These fire once on release for every direction that crossed tolerance during the gesture. A gesture that reverses can therefore fire more than one directional complete event. Use lockAxis: true when only one axis should qualify.

Event Fires when
UpComplete Upward movement was activated before release
DownComplete Downward movement was activated before release
LeftComplete Leftward movement was activated before release
RightComplete Rightward movement was activated before release

Change Events

Event Fires when
Change Any gesture movement occurs (any direction)
ChangeX Horizontal movement detected
ChangeY Vertical movement detected

Toggle Events

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

Event Fires when
ToggleX Gesture changes horizontal direction (left ↔ right)
ToggleY Gesture changes vertical direction (up ↔ down)

Press & Drag Events

Event Fires when
PressInit Immediately on press, before start position is recorded. No delta available yet
Press After start position is recorded (delta is available)
Release On pointer/touch release
Drag During active drag movement (after dragMinimum threshold is exceeded)
DragEnd When drag movement ends

Stop & Hover Events

Event Fires when
Stop Once after stopDelay ms of inactivity. Useful for “idle” state resets
Hover Pointer enters the target element. Requires a target element — does nothing on window
HoverEnd Pointer 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.

Action Description
'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 on Scroll direction Scroll position
Fires When user scrolls up or down When element reaches a scroll position
Typical use Navigation, slide transitions, direction-aware effects Reveal 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.