A custom cursor in React is a fixed-position element that follows the pointer, plus a decision about whether to hide the real one. The element is easy. The decision is where most implementations go wrong: the system cursor is a control surface rather than decoration, and on a touch device it does not exist at all. The short version is to build cursor-reactive effects freely, replace the pointer rarely, and never hide the native one without a fallback.
There are 69 public components on 21st with "cursor" in the name, and they are two different things wearing one label. A few replace the pointer with your own drawing. Most leave the pointer alone and use its position to drive something else: a spotlight, a trail, text that reacts as you pass over it. The second family carries almost none of the risk of the first, which is worth knowing before you pick.
Cursor replacements
Morphing Cursor by Jatin Yadav (975 bookmarks) is the canonical version: a dot that grows, shrinks and changes shape depending on what it is over. That state change is the entire argument for replacing a cursor, because it can say things the system pointer cannot, like "this card is draggable" or "click to play".
Fluid Magnetic Cursor by Hossain Jahed (388) adds attraction: the follower pulls toward a button as you approach, which makes small targets feel larger than they are. TubeCursor by Rahil Vahora (449) and Splash Cursor (1,313) are the maximal end, a fluid simulation trailing the pointer. Read those as a statement on a landing page, not as something to put on a settings screen.
Image Cursor Trail by Umair Waheed (638) sits between the families: the pointer stays where it is, and images drop behind it as it moves. Portfolios have done it for years and it still works, because it shows work while the visitor moves the mouse anyway.




Cursor-reactive effects
The most saved component in this whole group is not a cursor at all. Spotlight Card by Hossain Jahed has 7,173 bookmarks: a card with a radial highlight that tracks the pointer across its surface. The system cursor is untouched, so nothing about hit targets or touch devices changes, and the card still reacts.
The pattern repeats down the list. Card Spotlight (508) and Spotlight (405) by Manu Arora, Feature Spotlight by Jatin Yadav (364) and Apple Spotlight by Samit Kapoor (360) are the same idea at four different scales.
Then the typographic ones. Text Cursor Proximity by danielpetho (684) varies weight and spacing per character by distance from the pointer. Cursor Driven Particles Typography by Harsh Jadhav (552) does the particle version of the same measurement. Cursor Wander Card by Mridul Dhamija (509) and Stacked Panels Cursor by shadway (1,118) tilt panels toward wherever you are pointing.
If you want cursor motion in the design but do not want the argument that comes with replacing the pointer, take one of these. They degrade to a static card on touch, which is correct behaviour rather than a bug.




When a custom cursor is the wrong choice
Four cases, all common.
Touch. There is no pointer on a phone or tablet, so a replaced cursor draws nothing and the listeners never meaningfully fire. Everything you shipped for it is dead weight over the wire. Gate the whole thing behind matchMedia("(pointer: fine)") and do not mount it otherwise.
Anything with dense hit targets. Pointer shape is how people read a page's affordances: the arrow, the hand over a link, the I-beam over selectable text, the resize handle on an edge. A single drawn dot flattens all of that into one shape. On a marketing page nobody notices. In a table, a form, an editor or a dashboard, you have deleted information people were using to aim.
Hiding the native cursor with no fallback. cursor: none does not only hide a picture. The pointer the OS draws is the one that respects the user's own accessibility settings: enlarged pointer size, high contrast, the shake-to-find gesture on macOS. If you hide it, your replacement has to survive every condition where it could fail: a thrown error in the effect, a scroll container that swallows pointer events, a slow first paint. The safe pattern is to apply cursor: none only after the follower has mounted and confirmed it is running, and to restore the real cursor over any control where precision matters.
Reduced motion. A trail, a lerped follower and a fluid splash are all motion. When prefers-reduced-motion: reduce is set, drop the interpolation and either pin the element to the exact pointer position or skip rendering it. The query is one line, and the people who set it are the ones most likely to feel the difference.
What it costs to run
The performance failure is specific and nearly universal in first drafts: a mousemove handler that calls setState with the new coordinates. Pointer events fire as fast as the browser will deliver them, so that is a render, a reconciliation and a commit per event, for a component whose only output is a transform. On a page that already renders something expensive, that is where your dropped frames come from.
The cheap version never re-renders at all. Hold the position in a ref, write it straight to a transform inside one requestAnimationFrame, and let the browser coalesce the rest.
Three details in there matter more than the maths. pointermove covers mouse, pen and touch with one listener. The pointer: fine check means the component costs nothing on a phone. And pointer-events: none with aria-hidden keeps the element from ever intercepting a click or reaching a screen reader. If you want easing, keep the frame loop running and interpolate toward the latest pointer position inside it rather than adding a second timer.
A cursor-reactive effect can be even cheaper: write the pointer position to a CSS custom property on the card and let a gradient read it, so no React value changes at all.
Where else to look
Being honest about the alternatives, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
| MDN's cursor reference | Doing it in CSS with no JavaScript, including a custom image with a hotspot | No motion, no state changes, and image cursors have size limits per browser |
| shadcn/ui | The primitives and Tailwind base underneath whatever you build | No cursor or pointer effects at all; you write the effect yourself |
| React Bits | Copy-paste cursor and trail effects with demos you can try first | You own each file after copying it, so later fixes upstream never reach you |
| Aceternity UI | Spotlight and pointer-following cards in a consistent house style | Several depend on an animation runtime, a real bundle cost for one effect |
| 21st | Both families side by side, with a live preview before you take one | Quality varies by author, and installing needs a membership |
Taking one
Every component page has a live preview and the code, so you can move the pointer around before committing. Installing goes through the shadcn CLI against our registry:
The key comes from your 21st account and installs need a membership, so set API_KEY_21ST once in your shell and the same command works for anything in the catalogue. The 21st MCP does the same from Claude, Cursor or Codex if you would rather not leave the editor.
Whatever you take, check it twice: once with the pointer resting on a link, once on a phone. Those two checks catch nearly everything on this page.
Frequently asked
- How do you make a custom cursor in React?
- Render a fixed-position element, listen for pointermove on the window, and write the coordinates straight to that element's transform through a ref inside a requestAnimationFrame callback. Keeping the position in React state instead triggers a render, a reconciliation and a commit on every pointer event, which is the usual cause of dropped frames. Gate the component behind a (pointer: fine) media query so it never mounts where there is no pointer.
- Are custom cursors bad for accessibility?
- They can be. The pointer the operating system draws is the one that respects a user's own settings for pointer size, high contrast and the macOS shake-to-find gesture, and its shape (arrow, hand, I-beam, resize handle) is how people read what is clickable or selectable. If you replace it, apply cursor: none only once your follower is confirmed mounted and running, restore the real pointer over dense controls such as tables and forms, and drop the trailing motion when prefers-reduced-motion is set to reduce.
- Do custom cursors work on mobile?
- No. Touch devices have no pointer, so a replaced cursor draws nothing and its listeners never meaningfully fire, leaving whatever you shipped for it as unused code on a phone. Cursor-reactive effects like spotlight cards degrade cleanly instead, because the card simply stays in its resting state. Detect the difference with the (pointer: fine) media query rather than by sniffing the user agent.
- What is the difference between a custom cursor and a cursor effect?
- A custom cursor replaces the system pointer with your own element, which means hiding the native one and taking on its affordance and accessibility duties. A cursor effect only reads the pointer position to drive something else, such as a spotlight tracking across a card or text that reacts as you pass over it, and leaves the real cursor alone. The most saved component in the cursor group on 21st is a spotlight card rather than a cursor replacement, which makes the reactive family the safer default.