Beta Status

This library is still in early beta and the API is subject to change and may get daily breaking changes. The documentaton may not be up to date with the latest features and include missing or outdated information. You can always create an issue on GitHub or even better a pull request to fix the documentation or add new features.

Gestures

Motion components support hover, tap, focus, and viewport-triggered animations with corresponding event callbacks.

Hover

Animate while the pointer is over an element:

<motion.button
  whileHover={{ scale: 1.05, "box-shadow": "0 4px 12px rgba(0,0,0,0.15)" }}
  onHoverStart={() => console.log("hover start")}
  onHoverEnd={() => console.log("hover end")}
>
  Hover me
</motion.button>

Hover animations smoothly blend back to the current state when the pointer leaves.

Tap

Animate while pressing (pointer down):

<motion.button whileTap={{ scale: 0.95 }} onTap={() => console.log("tapped!")}>
  Tap me
</motion.button>

Tap callbacks: onTapStart (pointer down), onTap (pointer up inside), onTapCancel (pointer leaves or canceled).

By default, tap detection uses the element itself. Set globalTapTarget to capture pointer events at the document level.

Focus

Animate while an element has focus:

<motion.input
  whileFocus={{ scale: 1.02, "box-shadow": "0 0 0 2px blue" }}
  onFocus={() => console.log("focused")}
  onBlur={() => console.log("blurred")}
/>

Works with any focusable element: input, textarea, button, and elements with tabindex.

Viewport

Animate when an element enters or leaves the viewport:

<motion.div
  initial={{ opacity: 0, y: 50 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, amount: 0.5 }}
  onViewportEnter={(entry) => console.log("entered", entry)}
  onViewportLeave={(entry) => console.log("left", entry)}
/>

Viewport options:

  • once - Only trigger once
  • amount - Threshold for triggering ("some", "all", or number 0-1)
  • margin - Root margin (e.g., "100px")
  • root - Custom root element for IntersectionObserver
// Trigger when 50% visible
<motion.div whileInView={{ opacity: 1 }} viewport={{ amount: 0.5 }} />

// Trigger with offset margin
<motion.div whileInView={{ opacity: 1 }} viewport={{ margin: "-100px" }} />

Pan

Track pointer movement across an element:

<motion.div
  onPanSessionStart={(e, info) => console.log("session start", info)}
  onPanStart={(e, info) => console.log("pan start", info)}
  onPan={(e, info) => console.log("panning", info)}
  onPanEnd={(e, info) => console.log("pan end", info)}
/>

Pan info object contains: point (current position), delta (movement since last frame), offset (total movement from start), velocity (current velocity).

Example: Swipe Detection

function Swipeable() {
  const [offset, setOffset] = createSignal(0);

  const handlePanEnd = (e, info) => {
    if (Math.abs(info.offset.x) > 100) {
      setOffset(info.offset.x > 0 ? 200 : -200);
    } else {
      setOffset(0);
    }
  };

  return (
    <motion.div
      animate={{ x: offset() }}
      onPanEnd={handlePanEnd}
      style={{ touchAction: "pan-y" }}
    />
  );
}

Gesture Precedence

When multiple gestures are active, they layer in this order: Drag (highest), Pan, Tap, Hover (lowest).

Example: Interactive Card

function InteractiveCard(props: { title: string }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      whileHover={{ y: -5, "box-shadow": "0 8px 24px rgba(0,0,0,0.12)" }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 30 }}
    >
      <h3>{props.title}</h3>
    </motion.div>
  );
}

Example: Scroll Reveal

function ScrollReveal(props: { children: JSX.Element }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 30 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.3 }}
      transition={{ duration: 0.5 }}
    >
      {props.children}
    </motion.div>
  );
}