Shots in this scene
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.
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
Drag with no elasticity
Set
dragElastic={0}anddragMomentum={false}. Measured with a pointer pulled 400 px past the start: the square stopped at 170 and stayed there on release. - 2
Give the focus ring a twin
Delete
whileFocusand 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 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.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
can hover: true, fine pointer: trueuseScroll tracks this box, useSpring smooths the progress, useTransform turns it into a colour. No React state changes while you scroll.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
scrollTop at load: 0Break 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).
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.
- ▸
whileFocusfollows:focus-visible, so a mouse click does not trigger it but keyboard focus does. UsingwhileHoverandwhileFocuswith the same target gives both groups the same feedback. - ▸
A motion value fed by
useScrolldoes not cause React renders. ChaininguseTransformanduseSpringon 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. ReaduseReducedMotion()and passdragElastic={0}anddragMomentum={false}for a calmer release.
Remember this
- 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
Hover, tap, focus.
whileHover,whileTapandwhileFocustake a target likeanimate, apply it while the gesture lasts and return to the resting values after. Measured in the first example, with synthetic events: a mousepointerenteranimatedscaleto 1.1 over 233 ms (a critically damped spring); a touchpointerenterchanged nothing and logged nothing; apointerdownfrom either mouse or touch animated to 0.92; andfocus()animated to 1.1. The source explains it: hover ignores touch pointers and ignores pointers while a drag is active, andwhileFocusapplies only when the element matches:focus-visible, so a mouse click does not trigger it and keyboard focus does. - 3
Drag.
dragenables both axes,drag="x"one.dragConstraintsis 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 withdragMomentum(default on) a flick carries it before it settles.dragElastic={0}stops it exactly at the edge (measured: 170) anddragMomentum={false}removes the carry. - 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-yon a horizontal drag keeps vertical scrolling,touch-action: nonetakes all gestures. A drag inside a scroll area also needsdragDirectionLock, 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
Scroll as a value.
useScroll()returns motion values for the page's scroll:scrollYin pixels andscrollYProgressfrom 0 to 1.useScroll({ container: ref })tracks one scroll box, as in the second example.useTransformmaps ranges ([0, 1]to colours or lengths), anduseSpringsmooths any motion value with physics. Measured: halfway down the box the bar'sscaleXwas 0.5 and its colour had mixed torgb(149, 132, 144); at the end the transform wasnone(scale 1) and the colour wasrgb(42, 157, 244). - 6
Edge cases. The bar's
scaleXgoes throughuseSpring, so JavaScript writes it each frame (no track), while the colour, a plainuseTransformof the scroll, shows as oneScrolltrack: 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 CSSanimation-timelineof Phase 12 runs off the main thread. A page too short to scroll leavesscrollYProgressat 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
Why does whileHover do nothing on a phone, and how should you design for that?
How do dragConstraints and dragElastic work together?
Practice
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.
Add a reading-progress bar for the whole page.
Done when you can
You can use
whileHover,whileTapandwhileFocus, and say what each ignoresYou can constrain a drag and explain
dragElasticYou can set
touch-actionfor a drag so scrolling still worksYou can bind scroll to a style with
useScroll,useTransformanduseSpring