Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 15.5Advanced ~20 min · shot 5 of 7

Gestures and Scroll

In one line

whileHover, whileTap, whileFocus and drag animate an element in response to input, and useScroll turns a scroll position into a motion value you can map, smooth and bind to styles, all without React state.

0/7 · 0%

Think of it like this

A light switch with a dimmer knob. A switch reacts to a touch (on or off). A knob follows your hand all the way, and a spring-loaded knob goes back when you let go. Gestures are the switch (hover, tap, focus), drag is the knob, and useScroll is a knob turned by the page's scroll bar.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

gesture
A user action Motion recognises and animates for: hover, tap (press), focus, drag.
dragConstraints
The limits a dragged element may move within, as pixels or as a parent element.
dragElastic
How far past the constraints an element follows the pointer, from 0 (not at all) to 1 (fully).
touch-action
A CSS property that tells the browser which touch gestures it may handle itself (scrolling) and which it must leave to your code.
scrollYProgress
A motion value from 0 to 1: how far through the scrollable range the scroll position is.
:focus-visible
A CSS state for elements focused in a way that should show a focus ring, such as keyboard focus.

Step by step

01Hover and focus

Run the first example and press Tab: the Press button gets the browser's outline and scales to 1.1, because focus came from the keyboard. Click it with a mouse: it scales while the pointer is over it (hover) and to 0.92 while pressed (tap).

02Drag past the end

Drag the Drag square to the right end of its rail and keep going. It follows at a fifth of your speed beyond 170 px, then springs back when you let go. The console prints the offset your pointer travelled.

03Scroll the box

In the second example scroll the box. The bar grows from 0 to the full width and changes colour from orange to blue. The bar lags behind the scroll slightly because useSpring smooths it.

04Check on a touch device

Hover effects do nothing on a touch screen by design. Do not hide information behind whileHover: give it a whileTap or a visible control.

Try it yourself

  1. 1

    Drag with no elasticity

    Set dragElastic={0} and dragMomentum={false}. Measured with a pointer pulled 400 px past the start: the square stopped at 170 and stayed there on release.

  2. 2

    Give the focus ring a twin

    Delete whileFocus and Tab to the button. The browser's outline still appears, but the button no longer scales: keyboard users lose the motion that mouse users get from hover.

Code & diagrams

Hover, tap, focus and drag

Hover and tap scale the button; Tab to it for the focus scale. Drag the square: it is elastic past 170 px. The console reports whether your device can hover at all.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

can hover: true, fine pointer: true
Scroll progress as a motion value

useScroll tracks this box, useSpring smooths the progress, useTransform turns it into a colour. No React state changes while you scroll.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

scrollTop at load: 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Drag on a touch screen without touch-action

Remove touch-action: none from .box in the example's CSS and drag the square with a finger (a phone, or DevTools' touch emulation).

terminal
$ Drag the square sideways on a touch device
── what you'll see ──
The browser claims the gesture and scrolls or cancels it, so the drag stops after a few pixels (not reproducible in the preview here, which has no touch).

Myth vs fact

Myth

whileHover makes a button usable on mobile too.

Fact

Motion ignores touch pointers for hover, on purpose. A hover-only effect shows nothing on a phone, so the information must be visible without it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    whileFocus follows :focus-visible, so a mouse click does not trigger it but keyboard focus does. Using whileHover and whileFocus with the same target gives both groups the same feedback.

  • ▸

    A motion value fed by useScroll does not cause React renders. Chaining useTransform and useSpring on it costs only style writes, so scroll-linked effects stay cheap in React, but each frame still runs on the main thread: a long task there makes the effect lag.

  • ▸

    Under reduced motion, drag keeps working (it is input), but the release spring is not skipped. Measured with MotionConfig reducedMotion="user" and a reduced device: 40 ms after release the square was at 213 px and at 120 ms 201 px, exactly as without it. Read useReducedMotion() and pass dragElastic={0} and dragMomentum={false} for a calmer release.

Remember this

  1. 1

    The problem this solves: pointer handlers in React mean onMouseEnter, state, a class and a transition for every effect, plus separate code for touch and keyboard. Gesture props describe the *result* and Motion attaches, ignores and removes the right listeners. The cost of getting this wrong is a button that stays "hovered" on a phone, or a focus ring with no matching motion.

  2. 2

    Hover, tap, focus. whileHover, whileTap and whileFocus take a target like animate, apply it while the gesture lasts and return to the resting values after. Measured in the first example, with synthetic events: a mouse pointerenter animated scale to 1.1 over 233 ms (a critically damped spring); a touch pointerenter changed nothing and logged nothing; a pointerdown from either mouse or touch animated to 0.92; and focus() animated to 1.1. The source explains it: hover ignores touch pointers and ignores pointers while a drag is active, and whileFocus applies only when the element matches :focus-visible, so a mouse click does not trigger it and keyboard focus does.

  3. 3

    Drag. drag enables both axes, drag="x" one. dragConstraints is a box in pixels ({ left: 0, right: 170 }) or a ref to a parent element. Past the constraint the element resists: dragElastic (0.5 by default, 0.2 in the example) is the fraction of extra pointer movement it follows. Measured with a pointer pulled 260 px and 400 px: the element was at 188 px and 216 px, which is 170 plus 0.2 times the overshoot of 90 and 230. On release it springs back to 170 (measured, in real time), and with dragMomentum (default on) a flick carries it before it settles. dragElastic={0} stops it exactly at the edge (measured: 170) and dragMomentum={false} removes the carry.

  4. 4

    Touch and scroll conflict. On a phone the browser scrolls the page when a finger moves unless the element says otherwise. Motion's drag code sets no touch-action (nothing in its source does), so set it in CSS: touch-action: pan-y on a horizontal drag keeps vertical scrolling, touch-action: none takes all gestures. A drag inside a scroll area also needs dragDirectionLock, so the first clear direction wins. Give every drag a keyboard twin (buttons, or arrow keys that change the same state), because dragging has no keyboard equivalent by itself.

  5. 5

    Scroll as a value. useScroll() returns motion values for the page's scroll: scrollY in pixels and scrollYProgress from 0 to 1. useScroll({ container: ref }) tracks one scroll box, as in the second example. useTransform maps ranges ([0, 1] to colours or lengths), and useSpring smooths any motion value with physics. Measured: halfway down the box the bar's scaleX was 0.5 and its colour had mixed to rgb(149, 132, 144); at the end the transform was none (scale 1) and the colour was rgb(42, 157, 244).

  6. 6

    Edge cases. The bar's scaleX goes through useSpring, so JavaScript writes it each frame (no track), while the colour, a plain useTransform of the scroll, shows as one Scroll track: Motion hands it to the browser's own scroll timeline when it can (measured). A JavaScript-driven value like the spring is right when it must follow the scroll position with physics. For a pure scroll-driven effect with no JavaScript needs, the CSS animation-timeline of Phase 12 runs off the main thread. A page too short to scroll leaves scrollYProgress at 0. A pointer that is still while the page scrolls under it triggers no hover change until the pointer moves in most browsers, so do not rely on hover to show state.

Explain it without notes

01

Why does whileHover do nothing on a phone, and how should you design for that?

02

How do dragConstraints and dragElastic work together?

Practice

01

Make a swipe-to-dismiss card that moves with the finger on the x axis and calls onDismiss if released more than 120 px from where it started, otherwise springs back.

02

Add a reading-progress bar for the whole page.

Done when you can

  • You can use whileHover, whileTap and whileFocus, and say what each ignores

  • You can constrain a drag and explain dragElastic

  • You can set touch-action for a drag so scrolling still works

  • You can bind scroll to a style with useScroll, useTransform and useSpring