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>
PropTriggered byNotes
whileHoverPointer enters/leavesTrue-hover gating (touch devices don’t trigger)
whileTapPress start/end on elementKeyboard accessible (Enter/Space)
whileFocusFocus enters/leavesPairs with :focus-visible for accessibility
whileInViewElement enters/leaves viewportBacked 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

OptionTypeDefaultDescription
oncebooleanfalseFire on the first enter and latch — the element never reverts to its initial state
amountnumber \| 'some' \| 'all''some'Fraction of the element that must be visible (number 01) or named threshold
marginstring'0px'CSS margin string applied to the IntersectionObserver root box (e.g. '-50px 0px')
rootHTMLElementviewportCustom 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:

  • useInViewIntersectionObserver-backed in-view detection without a motion component
  • whileHover, whileTap, and whileFocus are 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.