Cursor Label Follower
A contextual chip that trails the pointer and names whatever it is over.
- Category
- Components
- Added
- 2026-09-20
- Deps
- 1
- Updated
- 2026-09-20
Props
"use client";
import { useRef, useState } from "react";
import { motion, useMotionValue, useSpring } from "framer-motion";
export function CursorLabelFollower({
children,
stiffness = 420,
showDot = true,
}: {
children: React.ReactNode;
stiffness?: number;
/** The small leading dot inside the chip. */
showDot?: boolean;
}) {
const boxRef = useRef<HTMLDivElement>(null);
const [label, setLabel] = useState<string | null>(null);
// Motion values, not state: pointer movement writes a transform directly
// and never re-renders this component.
const x = useMotionValue(0);
const y = useMotionValue(0);
const sx = useSpring(x, { stiffness, damping: 30, mass: 0.35 });
const sy = useSpring(y, { stiffness, damping: 30, mass: 0.35 });
function handleMove(e: React.MouseEvent<HTMLDivElement>) {
const rect = boxRef.current?.getBoundingClientRect();
if (!rect) return;
x.set(e.clientX - rect.left);
y.set(e.clientY - rect.top);
}
// Any descendant with data-cursor-label="Something" sets the chip text.
function handleOver(e: React.MouseEvent<HTMLDivElement>) {
const target = (e.target as HTMLElement).closest("[data-cursor-label]");
setLabel(target?.getAttribute("data-cursor-label") ?? null);
}
return (
<div
ref={boxRef}
onMouseMove={handleMove}
onMouseOver={handleOver}
onMouseLeave={() => setLabel(null)}
className="relative"
>
{children}
<motion.div
aria-hidden
style={{ x: sx, y: sy }}
className="pointer-events-none absolute left-0 top-0 z-50"
>
<motion.div
initial={false}
animate={{ opacity: label ? 1 : 0, scale: label ? 1 : 0.7 }}
transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}
className="flex -translate-x-1/2 -translate-y-1/2 items-center gap-1.5 rounded-full bg-blue-600 px-2.5 py-1.5 font-mono text-[10px] uppercase leading-none tracking-[0.12em] text-white"
>
{showDot && <span className="size-1 rounded-full bg-white/70" />}
{label ?? ""}
</motion.div>
</motion.div>
</div>
);
}
About this component
A cursor chip that names what it is over does something most custom cursors do not: it carries information. On a portfolio grid or a media gallery, where every tile looks clickable but they do not all do the same thing, it replaces a row of visible button labels with one that only appears where the pointer already is. The mechanism worth copying is the delegation. Rather than attaching enter and leave handlers to every target, one onMouseOver on the wrapper reads event.target.closest with a data attribute selector, so adding a new labelled element is an attribute and nothing else — it scales to a hundred tiles at the cost of one listener. Position rides Framer motion values with a spring, never state, which is what keeps a grid of mounted previews from re-rendering on every pixel of pointer travel. Two caveats: the chip is decorative and hidden from assistive technology, so whatever it says must also exist in the accessible name of the thing it labels, and on touch devices there is no pointer at all, so the underlying affordance has to stand on its own.
Curator’s note
Rebuilt in-house for viberdy 2.0. The delegated data-attribute lookup is the part worth keeping — it scales to a whole gallery without one handler per tile.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Snapping Cursor
componentsA cursor dot that latches onto interactive elements and takes their exact shape.
Magnetic Button
componentsA button whose label leans toward the cursor with a springy pull.