Skip to content

Snapping Cursor

A cursor dot that latches onto interactive elements and takes their exact shape.

Freeminimalplayfuleditorial
Category
Components
Added
2026-09-20
Deps
1
Updated
2026-09-20
Preview

Props

10
6
Show idle dot
Requires framer-motion
"use client";

import { useRef, useState } from "react";
import { motion, useMotionValue, useSpring } from "framer-motion";

export function CursorSnapTarget({
  children,
  idleSize = 10,
  padding = 6,
  showIdleDot = true,
}: {
  children: React.ReactNode;
  idleSize?: number;
  padding?: number;
  /** Keep the dot visible in open space. Off, it only appears when snapped. */
  showIdleDot?: boolean;
}) {
  const boxRef = useRef<HTMLDivElement>(null);
  const [snapped, setSnapped] = useState(false);

  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const w = useMotionValue(idleSize);
  const h = useMotionValue(idleSize);

  const spring = { stiffness: 480, damping: 36, mass: 0.6 };
  const sx = useSpring(x, spring);
  const sy = useSpring(y, spring);
  const sw = useSpring(w, spring);
  const sh = useSpring(h, spring);

  function handleMove(e: React.MouseEvent<HTMLDivElement>) {
    // While snapped the cursor is pinned to the target's box, so free
    // tracking is suspended until the pointer leaves it.
    if (snapped) return;
    const rect = boxRef.current?.getBoundingClientRect();
    if (!rect) return;
    x.set(e.clientX - rect.left - idleSize / 2);
    y.set(e.clientY - rect.top - idleSize / 2);
    w.set(idleSize);
    h.set(idleSize);
  }

  // Delegated: any descendant marked data-cursor-snap becomes a target.
  function handleOver(e: React.MouseEvent<HTMLDivElement>) {
    const el = (e.target as HTMLElement).closest<HTMLElement>("[data-cursor-snap]");
    const host = boxRef.current?.getBoundingClientRect();
    if (!el || !host) {
      setSnapped(false);
      return;
    }
    const target = el.getBoundingClientRect();
    x.set(target.left - host.left - padding);
    y.set(target.top - host.top - padding);
    w.set(target.width + padding * 2);
    h.set(target.height + padding * 2);
    setSnapped(true);
  }

  return (
    <div
      ref={boxRef}
      onMouseMove={handleMove}
      onMouseOver={handleOver}
      onMouseLeave={() => setSnapped(false)}
      className="relative cursor-none"
    >
      {children}

      <motion.div
        aria-hidden
        style={{ x: sx, y: sy, width: sw, height: sh }}
        animate={{
          borderRadius: snapped ? 8 : 999,
          opacity: snapped || showIdleDot ? 1 : 0,
        }}
        transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
        className="pointer-events-none absolute left-0 top-0 border border-blue-500 bg-blue-500/15"
      />
    </div>
  );
}
Notes

About this component

Most magnetic cursors scale up when they near a button and keep chasing the pointer underneath, which reads as a wobble rather than a lock. This one commits: on entering a target it adopts that element's measured bounding box and then stops tracking the pointer entirely until the pointer leaves. That suspension is the whole effect — because the cursor is genuinely pinned rather than converging, the result feels like a physical detent, and it doubles as a real affordance, since the outline states exactly which hit area is live. Width and height ride springs alongside x and y, so the circle-to-rectangle morph is one continuous motion rather than a crossfade between two shapes. Reach for it on a small number of high-intent controls; across a dense interface the constant re-shaping becomes noise, and this library's Cursor Label Follower is the better fit when the goal is naming targets rather than outlining them. Because it sets `cursor: none` and the indicator is decorative, keyboard focus styles have to stand on their own — the preview wires focus and blur to the same snap for exactly that reason.

Curator’s note

Built for viberdy 2.0. Suspending pointer tracking while snapped is the detail that separates this from a cursor that merely grows near a button.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
New

A contextual chip that trails the pointer and names whatever it is over.

cursorlabelspring
Featured

Magnetic Button

components

A button whose label leans toward the cursor with a springy pull.

buttoncursorspring

Tilt Card

components

A 3D card that tilts toward the cursor with spring physics.

card3dcursor