Gestures
Every motion.<tag> component accepts four gesture props that animate to a target state for the duration of the gesture and restore on exit. They compose freely with initial, animate, exit, and transition.
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ scale: 1.05, borderColor: '#3b82f6' }}
>
Click me
</motion.button><motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ scale: 1.05, borderColor: '#3b82f6' }}
>
Click me
</motion.button>| Prop | Triggered by | Notes |
|---|---|---|
whileHover | Pointer enters/leaves | True-hover gating (touch devices don’t trigger) |
whileTap | Press start/end on element | Keyboard accessible (Enter/Space) |
whileFocus | Focus enters/leaves | Pairs with :focus-visible for accessibility |
whileInView | Element enters/leaves viewport | Backed by IntersectionObserver |
Each gesture prop has companion lifecycle callbacks (onHoverStart / onHoverEnd, onTapStart / onTap / onTapCancel, onFocusStart / onFocusEnd, onInViewStart / onInViewEnd) for side effects that aren’t expressible as style changes (analytics, focus management, etc.).
whileHover
Animate while the pointer is over the element.
<motion.button
whileHover={{ scale: 1.05, backgroundColor: '#3b82f6' }}
onHoverStart={() => console.log('hover start')}
onHoverEnd={() => console.log('hover end')}
>
Hover me
</motion.button><motion.button
whileHover={{ scale: 1.05, backgroundColor: '#3b82f6' }}
onHoverStart={() => console.log('hover start')}
onHoverEnd={() => console.log('hover end')}
>
Hover me
</motion.button>True-hover gating. whileHover uses CSS media queries ((hover: hover) and (pointer: fine)) to gate the hover state. Touch devices that emit synthetic hover events on tap don’t trigger the gesture — the hover animation stays inert on phones and tablets, matching how :hover CSS pseudo-class behaves in modern stylesheets.
whileTap
Animate while the element is being pressed.
<motion.button
whileTap={{ scale: 0.95 }}
onTapStart={() => console.log('press down')}
onTap={() => console.log('press complete (no cancel)')}
onTapCancel={() => console.log('pointer left before release')}
>
Tap me
</motion.button><motion.button
whileTap={{ scale: 0.95 }}
onTapStart={() => console.log('press down')}
onTap={() => console.log('press complete (no cancel)')}
onTapCancel={() => console.log('pointer left before release')}
>
Tap me
</motion.button>Keyboard accessible. Pressing Enter or Space on a focused element fires the same tap lifecycle as a pointer press. Combine with whileFocus to make tap interactions discoverable for keyboard users.
onTap fires only when the press completes on the element. If the pointer drags off before release, onTapCancel fires instead.
whileFocus
Animate while the element has keyboard focus.
<motion.input
whileFocus={{ scale: 1.02, borderColor: '#3b82f6' }}
onFocusStart={() => console.log('focus enter')}
onFocusEnd={() => console.log('focus leave')}
/><motion.input
whileFocus={{ scale: 1.02, borderColor: '#3b82f6' }}
onFocusStart={() => console.log('focus enter')}
onFocusEnd={() => console.log('focus leave')}
/>Pairs well with whileHover on inputs and buttons so the focus animation matches what hover feels like — important for users who navigate with Tab instead of pointer.
whileInView
Animate when the element enters the viewport. Backed by IntersectionObserver.
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5, margin: '-50px' }}
transition={{ duration: 0.6 }}
onInViewStart={() => console.log('entered viewport')}
onInViewEnd={() => console.log('left viewport')}
>
Fades in on scroll
</motion.div><motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5, margin: '-50px' }}
transition={{ duration: 0.6 }}
onInViewStart={() => console.log('entered viewport')}
onInViewEnd={() => console.log('left viewport')}
>
Fades in on scroll
</motion.div>viewport options
| Option | Type | Default | Description |
|---|---|---|---|
once | boolean | false | Fire on the first enter and latch — the element never reverts to its initial state |
amount | number \| 'some' \| 'all' | 'some' | Fraction of the element that must be visible (number 0–1) or named threshold |
margin | string | '0px' | CSS margin string applied to the IntersectionObserver root box (e.g. '-50px 0px') |
root | HTMLElement | viewport | Custom scroll container — defaults to the page viewport |
once: true for reveal animations prevents the “scroll up, see it again” effect that’s usually unwanted for content-reveal patterns.
Composing with transition
Every gesture prop accepts an optional transition inside the target object so a gesture can have its own timing distinct from the component’s default transition:
<motion.div
transition={{ duration: 0.3 }}
whileHover={{
scale: 1.05,
transition: { duration: 0.15 }
}}
whileTap={{
scale: 0.95,
transition: { type: 'spring', stiffness: 800 }
}}
/><motion.div
transition={{ duration: 0.3 }}
whileHover={{
scale: 1.05,
transition: { duration: 0.15 }
}}
whileTap={{
scale: 0.95,
transition: { type: 'spring', stiffness: 800 }
}}
/>This gives quick feedback on hover and a snappy spring on tap while keeping the default for any non-gesture animation.
Hook variants
For non-component cases (vanilla DOM, custom logic that isn’t a <motion.div>), use the hook form where one exists:
useInView—IntersectionObserver-backed in-view detection without a motion componentwhileHover,whileTap, andwhileFocusare best used via the motion-component props. There’s no dedicated hook for those three because the prop form is the recommended path; if you need the same behaviour outside a motion component, wire up a native listener (pointerenter/leave,pointerdown/up,focus/blur) directly.
Based on Motion’s gesture animations API.