---
title: Gestures
description: Hover, tap, focus, in-view, pan and drag gestures, with Motion's gesture props.
---

## Overview

Mercury supports Motion's gestures through the same props. Each `while*` prop is a target (or [variant](/docs/api/variants) label) that is active while the gesture is: Motion animates to it, and back to whatever is underneath when the gesture ends. Gestures stack by priority — `animate` < `whileInView` < `whileFocus` < `whileHover` < `whileTap` < `whileDrag` < `exit` — so a press on a hovered button animates from the hover state and returns to it on release.

Each target can have its own `transition`; otherwise the attachment's `transition` is used.

## Hover

- `whileHover`: target while the pointer is over the element.
- `onHoverStart(event, info)` / `onHoverEnd(event, info)`.

Hover ignores touch input, so a tap on a phone doesn't leave the element stuck in its hover state.

```svelte
<div
	{@attach mercury({
		whileHover: { scale: 1.1, transition: { type: 'spring', stiffness: 150, damping: 20 } }
	})}
/>
```

<Component path="hover" />

## Tap

- `whileTap`: target while the element is pressed.
- `onTapStart(event, info)` — the press began.
- `onTap(event, info)` — the press ended on the element.
- `onTapCancel(event, info)` — the pointer was released outside it.
- `globalTapTarget`: end the press wherever the pointer is released.
- `propagate: { tap: false }`: stop a press on this element from also pressing its ancestors.

Presses work from the keyboard too: Enter presses a focused element. Like Motion, Mercury makes a tappable element focusable (`tabindex="0"`) if it isn't already. A disabled `<button>` doesn't respond.

```svelte
<button {@attach mercury({ whileTap: { scale: 0.95 }, onTap: () => console.log('tapped') })}>
	Press me
</button>
```

<Component path="tap" />

## Focus

`whileFocus` is active while the element has focus that the browser would show (`:focus-visible`) — keyboard focus, not a mouse click on a button.

```svelte
<button {@attach mercury({ whileFocus: { scale: 1.08 } })}>Focus me</button>
```

<Component path="focus" />

## In view

- `whileInView`: target while the element is in the viewport (or `viewport.root`).
- `onViewportEnter(entry)` / `onViewportLeave(entry)`.
- `viewport`:
  - `once`: stay in the in-view state after the first time.
  - `root`: the scrollable ancestor to observe, as an element (e.g. from `bind:this`) or `{ current }`.
  - `margin`: grow or shrink the detection area, like CSS margin (`'0px 0px -100px 0px'`).
  - `amount`: how much of the element must be visible: `'some'` (default), `'all'`, or a 0–1 fraction.

```svelte
<div
	{@attach mercury({
		initial: { opacity: 0, y: 24 },
		whileInView: { opacity: 1, y: 0 },
		viewport: { once: true, amount: 0.5 }
	})}
/>
```

<Component path="scroll" />

For animations linked to scroll position, see [Motion values](/docs/api/motion-values#scroll).

## Pan

Pan reports a pointer moving across the element, without moving it:

- `onPanStart(event, info)`, `onPan(event, info)`, `onPanEnd(event, info)`, `onPanSessionStart(event, info)`.
- `info` holds `point`, `delta`, `offset` (from the start) and `velocity`, in pixels.

Pan starts after the pointer moves 3px. For touch input, add `touch-action: none` (or `pan-y`…) to the element so the browser doesn't scroll instead.

## Drag

Set `drag: true` to make an element draggable, or `drag: 'x'` / `'y'` to lock it to one axis. Mercury runs Motion's drag gesture: the element follows the pointer through its `x` / `y` motion values, so it composes with every other animation, and flings with inertia when released.

- `whileDrag`: target while dragging.
- `dragConstraints`: an element (e.g. from `bind:this`) or `{ current }` ref the element must stay inside, or pixel offsets from its position: `{ left: -100, right: 100, top: 0, bottom: 0 }`.
- `dragElastic`: how far it can be pulled past its constraints: `0` (none) to `1` (freely), default `0.35`. Also `{ top, left, right, bottom }` per side, or `false`.
- `dragMomentum`: fling with inertia on release (default `true`).
- `dragTransition`: the inertia options — `power`, `timeConstant`, `bounceStiffness`, `bounceDamping`, `min`, `max`, `modifyTarget` to snap to a grid…
- `dragSnapToOrigin`: animate back to the start on release (`true`, `'x'` or `'y'`).
- `dragDirectionLock`: lock to the axis the pointer first moves along. `onDirectionLock(axis)` reports it.
- `dragPropagation`: let a drag on this element also drag draggable ancestors.
- `dragListener: false`: only start drags through `dragControls`.
- `onMeasureDragConstraints(constraints)`: adjust the measured constraints.
- Events: `onDragStart(event, info)`, `onDrag(event, info)`, `onDragEnd(event, info)` and `onDragTransitionEnd()`.

```svelte
<script lang="ts">
	import { mercury } from '@omicrxn/mercury';
	let container = $state<HTMLElement>();
</script>

<div bind:this={container} class="boundary">
	<div
		{@attach mercury({
			drag: true,
			dragConstraints: container,
			dragElastic: 0.2,
			whileDrag: { scale: 1.1 }
		})}
		class="box"
	/>
</div>
```

<Component path="drag" />

Like Motion, Mercury sets `touch-action` (and disables text selection) on draggable elements, so touch drags don't scroll the page. Dragging works with constraints that move — they're measured when each drag starts and when the window or constraint element resizes.

Mercury also lets a focused draggable element move with the **arrow keys** (Shift for bigger steps), within its constraints. Set `dragKeyboard: false` to turn it off, or a number to change the 10px step.

### Drag controls

To start a drag from somewhere else — a handle, or a pointerdown anywhere on the page — create drag controls and pass them to the draggable element. This is Motion's `useDragControls()`:

```svelte
<script lang="ts">
	import { mercury, createDragControls } from '@omicrxn/mercury';
	const controls = createDragControls();
</script>

<div onpointerdown={(event) => controls.start(event)}>Handle</div>
<div {@attach mercury({ drag: true, dragControls: controls, dragListener: false })}>Card</div>
```

`controls.start(event, { snapToCursor: true })` centers the element on the pointer first. `controls.stop()` and `controls.cancel()` end the drag.

<Component path="drag-controls" />
