Every browser ships a cursor for free: instantly responsive, universally understood, accessible by default, invisible in the way only something perfectly familiar can be. Replacing it with a custom one is therefore not a decoration, it's a trade — you give up all of that for the chance to say something the default arrow can't. Most sites that make this trade lose it, because they treat the custom cursor as a cosmetic reskin rather than what it actually is: a new piece of interface that inherits every expectation the old one satisfied silently, and now has to satisfy them on purpose.
What you're actually replacing
The native cursor does more jobs than it gets credit for. It tells you where you are, with zero latency, because it's rendered by the compositor rather than by JavaScript reacting to a mousemove event. It changes shape at boundaries — a text caret over copy, a pointing hand over a link, a resize arrow at a window edge — communicating affordance without a single word. And it never breaks, because the operating system owns it, not a script that might not have loaded yet.
A custom cursor has to rebuild all of that from nothing, in userland, on the main thread, competing with everything else the page is doing. The instant it lags a frame behind the real pointer, or forgets to change shape over a text field, it stops being a nicer cursor and starts being a worse one wearing a nicer costume. The bar isn't 'does it look good in the demo.' It's 'does it hold up against the thing it replaced, which had years of platform engineering behind it and cost the user nothing to trust.'
The label is the point
If a custom cursor is only a re-skinned arrow, it wasn't worth building. The only honest justification for taking on the cost is that the cursor can now carry information the native one couldn't — a label that says 'View', 'Drag', 'Play', or a variant that swells and darkens over an image to signal it's clickable before the user has consciously registered a button underneath their pointer. We drive this with a simple data attribute on any interactive element — a text label and an optional variant — read by one hook that positions and mutates the cursor's own DOM node. The markup stays declarative; the interaction logic stays in one place instead of being reimplemented per component.
The discipline is restraint in what earns a label. A cursor that annotates everything says nothing, the same failure mode as a page where every word is bold. We reserve labels for the handful of interactions where the default click affordance is genuinely ambiguous — a full-bleed image that's secretly a link, a project row where the entire card is clickable but only the title looks like it. Anywhere the native cursor's shape already tells the whole story, we leave it alone and let the label rest.
The lag that ruins it
Setting a cursor element's position directly from a mousemove handler, one-to-one, sounds like it should be free of latency — the value updates every event. In practice it still trails the real pointer by a frame or more once React re-renders or the main thread is doing anything else, and that trail is exactly what makes a custom cursor read as sluggish rather than premium. The fix isn't a faster event handler. It's decoupling the cursor's rendered position from React state entirely and driving it straight to the DOM on a rendering-loop tick — GSAP's quickTo utility, or an equivalent tween-to-target function, so the cursor eases toward the pointer's real position every frame rather than snapping to a value React only got around to committing.
A custom cursor earns its keep the instant it stops feeling like a cursor and starts feeling like the pointer itself, just slightly alive.
That 'slightly alive' quality — a touch of lag, not React's lag but a deliberate, tuned ease — is the entire difference between a cursor that feels premium and one that feels like a tech demo. Too tight and it's indistinguishable from the native pointer, which defeats the purpose. Too loose and it reads as broken. The right amount of follow is a design decision, tuned by eye, not a default left at whatever a tutorial suggested.
What a custom cursor must never break
Setting cursor: none globally and drawing your own is an aesthetic choice with real casualties if you're not careful. Text selection is the first one: a user hovering an input or a paragraph still needs to see an I-beam, or they lose the single strongest piece of feedback that tells them text is selectable at all. Our cursor component checks the element under the pointer and falls back to the native cursor over text and form fields rather than trying to draw a custom text-caret equivalent, because the platform's version is better than anything we'd build to replace it.
Keyboard and touch users never see a cursor at all, and the implementation has to know that rather than assume a mouse. cursor: none applied unconditionally, with no fallback, makes a touch device unusable the instant an interactive element only shows its affordance via cursor hover — so every hover-driven cue also needs a non-hover equivalent, and :focus-visible outlines have to survive the redesign untouched, because a keyboard user tabbing through the page was never looking at the cursor in the first place. A custom cursor that quietly degrades keyboard accessibility to win a visual flourish has made a trade nobody agreed to.
The restraint test
Before a custom cursor ships, we ask one question: turn it off, and does the site get harder to use, or just less decorated? If it's only less decorated, the cursor was doing its job as an atmosphere layer and nothing was resting on it that shouldn't be. If turning it off breaks comprehension — a user can no longer tell what's clickable, or loses feedback a native cursor would have given for free — the custom cursor was carrying weight it was never sturdy enough to hold, and the fix is to move that job back onto the interface itself, where it belongs regardless of what the pointer looks like.
The arrow the browser gives you for free is not a placeholder waiting to be replaced. It's a fully engineered piece of interface that a custom cursor has to out-earn, not just out-decorate, before it's allowed to take its place.
Frequently asked questions
Does a custom cursor hurt accessibility?
Only if it's built carelessly. The two non-negotiables are a native fallback over text fields and selectable copy, and leaving :focus-visible outlines and all non-hover affordances fully intact for keyboard users, who never see the cursor at all. Built with both, a custom cursor adds a layer on top of accessible interaction rather than replacing it.
Why does my custom cursor feel laggy compared to the real pointer?
Usually because its position is being set through React state and a re-render cycle rather than written straight to the DOM on a rendering-loop tick. Driving position with a tween-to-target function like GSAP's quickTo, updated every frame outside React's render cycle, removes the extra latency and lets you tune a small, deliberate follow-lag by design instead of by accident.
Should every interactive element get a cursor label?
No — labelling everything makes the label meaningless. Reserve custom cursor labels for interactions where the default pointer shape is genuinely ambiguous, like a full card that's clickable but doesn't look like a button. Anywhere the native cursor already communicates the affordance clearly, leave it alone.