Skip to content

Cursor Follower

Add a cursor effect that follows the pointer and changes shape over controls, text and media.

import { CursorFollower } from '@youj/motion/interactions'

Installation & usage

npm i @youj/motion

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it on expressive pages, like a portfolio, a launch or a studio site, where the pointer itself is part of the design. It tells the reader what a target is before they click: a button, some text, a link to read or a project to open. Don't use it in apps and tools. People work there with the cursor they know, and a second one adds latency to every glance. Move the pointer over the preview: it needs a mouse or a trackpad.

Render one follower, scoped to the part of the page it serves, and mark its targets with data-cursor.

import { useRef } from 'react'
import { CursorFollower } from '@youj/motion/interactions'

export function Studio() {
  const scope = useRef<HTMLDivElement>(null)
  return (
    <div ref={scope}>
      <CursorFollower scope={scope} />
      <h1 data-cursor="text">We design software that feels inevitable.</h1>
      <button data-cursor="button">Start a project</button>
      <a href="/work/northwind" data-cursor="media">…</a>
    </div>
  )
}

Without scope, it follows the pointer across the whole document. It draws in a fixed layer on top of the page, portaled to body, and never takes pointer events, so everything under it stays clickable.

Reference

Propsscope, hideNativeCursor, disabled (3)

Props

CursorFollower props
PropType
scopeRefObject<HTMLElement | null>

Only react inside this element. Default: the whole document.

–
hideNativeCursorboolean

Hide the native cursor while the follower is drawing. Off by default. The native cursor is only hidden once the follower is active and visible, and comes back whenever the follower is disabled or the pointer leaves.

default false

false
disabledboolean

Turn the follower off. It is also off for touch / coarse pointers, reduced motion and the low motion-budget tier.

default false

false
MotionPosition, shape, fill and label · reduced motion

Motion

Motion tokens
MomentTokens
Positionfollows the pointer
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Shapewidth, height and corner radius
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Fill and labelcrossfades between kinds
spring.snappy

0.2s, bounce 0

0.2s, bounce 0

Reduced motion

Renders nothing. The native cursor does all the work.

Position and shape run on separate springs, so moving feels immediate while a change of shape feels deliberate. The shape is one box scaled on each axis, with its corners counter-scaled so they stay round mid-morph, and the label sits outside the scaled box so its text never stretches. Every update happens in the pointer handler, with no React render per move. The follower appears where the pointer first enters, instead of flying in from a corner, and when the page scrolls it re-reads what's under the pointer.

Reduced motion

The follower renders nothing at all, as it also does on the low motion-budget tier: a cursor that trails the pointer is motion for its own sake, and there's no static version that says anything the native cursor doesn't. If hideNativeCursor is set, the native cursor stays visible.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The layer is aria-hidden, ignores the pointer and never takes focus. Keyboard users never see it, and every target keeps its own focus ring.
  • Labels like "View" and "See work" are visual only. Give every link and button a name of its own in its content or with aria-label.
  • It renders nothing for touch and other coarse pointers, or on the low motion budget tier, and keeps the native cursor unless you ask otherwise with hideNativeCursor.