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
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 |
stopDelayis in milliseconds, not seconds. The default150means 150ms of inactivity triggersStop. 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:
// 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 if that direction was the dominant movement.
| Event | Fires when |
|---|---|
UpComplete | Gesture ended while moving upward |
DownComplete | Gesture ended while moving downward |
LeftComplete | Gesture ended while moving left |
RightComplete | Gesture ended while moving right |
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.
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.
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:
.onGesture({
types: ["wheel"],
events: { Up: "progressUp", Down: "progressDown" },
animationStep: { Up: 0.15, Down: 0.05 },
}) Touch Swipe with Live Demo
Slide Gallery with playNext / playPrevious
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.
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.
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.
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.
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 gestureHover/HoverEndevents 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.
// 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.
Related
- Scroll Trigger — position-based scroll animations with scrub, pin, and snap
- Scroll Trigger Advanced — pinning, horizontal scroll, and snap
- Click Trigger — toggle animations on click