Data attributes

The full markup contract: every data-cursor value, which variants respond to it, and the modifier attributes.

The cursor reacts to your markup, not to your component tree. Put a data-cursor attribute on an element and any mounted variant that understands that value will respond when the pointer enters it.

<button data-cursor="magnetic">Hire us</button>

Attributes are the public API on purpose. They work in MDX, in CMS content, in a server component, and in a template rendered by something that is not React at all — no import, no wrapper, no ref.

Values

ValueEffectResponds
growScales the follower up over the element.dot-ring, magnetic, spotlight, blend
magneticRing morphs onto the element's box and pulls the element toward the pointer.magnetic
blendScales the blend blob up.blend
hideFades the follower out entirely.all six
textReserved for a label-carrying variant. Currently a no-op.none yet

A variant that does not understand a value ignores it, so marking an element magnetic is safe on a page running trail-dots — nothing happens, nothing errors.

Modifiers

These refine the behaviour of the element they sit on. All are optional.

AttributeTypeMeaning
data-cursor-strength0–1How hard magnetic pulls this element. Defaults to the variant's strength prop.
data-cursor-colorany CSS colourOverrides the layer colour while this element is hovered.
data-cursor-textstringLabel for the reserved text behaviour.

data-cursor-strength is clamped to 0–1 when written through useCursorTarget. Out-of-range values in raw markup are clamped by the variant instead, so nothing flies off screen either way.

The typed helper

Raw attributes are stringly-typed, and data-cursor-stength is a typo TypeScript will never catch. useCursorTarget exists for that:

import { useCursorTarget } from "@/components/pointerui/provider";

function Hire() {
  return <button {...useCursorTarget({ variant: "magnetic", strength: 0.4 })}>Hire us</button>;
}

It returns the attribute object and nothing else. There is no runtime behaviour attached to it, so it is a convenience rather than a requirement — reach for it in code you compile and skip it in content you author.

How targets are found

One pointerover listener on the document runs event.target.closest('[data-cursor]'). Two things follow from that, and both are useful:

Nesting works. The nearest ancestor with the attribute wins, so wrapping a card in data-cursor="grow" covers everything inside it without marking each child.

Dynamic content works. Nothing is bound at mount, so an element that appears later — a modal, a lazy list, a client-side route — is a target the moment it exists. There are no per-element listeners to attach and none to leak.

Scoping who reacts

If you need one region of the page to behave differently, mount a second provider with a scope. Inside that element its engine owns the pointer; outside, the global one does. See Engine for the details — this is the same mechanism every preview on this site uses.