react-smart-mouse
Smart custom cursor for React & Next.js — a cursor follower that automatically adapts its shape, size and color to whatever it hovers.
Unlike a plain cursor follower, SmartMouse knows which element it's hovering — links, buttons, images, inputs, text — and morphs into a different style for each one. Drop it in once and your whole app gets an interactive, animated cursor.

Features
- Element-aware — automatically restyles itself over
a,button,img,input,textarea, checkboxes, radios, and more - React 16.14 – 19 and Next.js (Pages & App Router) — ships with
"use client", import it anywhere - Fast — position updates bypass React re-renders; a single injected style rule hides the native cursor
- Magnetic hover — the follower snaps to the center of elements you mark as magnetic
- Smooth trailing — optional lerp smoothing for that lagging-cursor feel
- Text labels — show "View", "Drag", etc. inside the cursor via a
mouselabelattribute - Custom cursor content — pass
childrento replace the dot with anything (emoji, rings, SVG) - Glass mode — frosted-glass cursor with blurred backdrop and translucent color tints
- Image magnifier — the cursor becomes a zoom lens over images marked with
mousemagnify - Check animation — the cursor pulses when a checkbox or radio is toggled
- Touch-aware — automatically disables itself on touch-only devices
- Accessible — respects
prefers-reduced-motionout of the box - Click feedback — optional press animation via
clickScale - Fully customizable — per-tag styles, per-class styles, any-CSS-selector styles, per-element
mousecustomoverrides, blend modes - Zero dependencies, ~2 kB min+gzip, TypeScript types included
Installation
npm install react-smart-mouse
Quick start
import SmartMouse from 'react-smart-mouse'
const App = () => (
<div>
{/* Your app */}
<SmartMouse />
</div>
)
That's it — the native cursor is replaced by an animated follower that reacts to every element it passes over.
Customized
<SmartMouse
// per-tag styles
pStyle={{ color: 'rgba(191, 75, 30, 1)', height: '40px', width: '40px' }}
aStyle={{ color: 'rgba(132, 245, 39, 0.8)', height: '20px', width: '20px' }}
buttonStyle={{ color: 'rgba(118, 39, 245, 0.8)', height: '20px', width: '20px' }}
// per-class styles
classStyle={{
'some-class': { width: '50px', height: '50px', color: '#996099' },
}}
// defaults for everything else
defaultWidth="35px"
defaultHeight="35px"
defaultColor="rgba(0, 0, 0, 0.5)"
// new in 2.1
clickScale={0.8}
blendMode="difference"
transitionDuration="0.5s"
lerp={0.15}
magneticSelector=".btn, .nav-link"
/>
Colors accept HEX, RGB(A), or named colors ('red').
Recipes
Smooth trailing cursor
lerp controls how quickly the follower catches up with the real cursor — 1 sticks to it, lower values trail behind:
<SmartMouse lerp={0.15} />
Magnetic elements
While hovered, magnetic elements pull the follower to their center. Mark them with an attribute or a selector (pairs beautifully with lerp):
<SmartMouse magneticSelector=".magnetic" lerp={0.2} />
<button className="magnetic">Hover me</button>
{/* or per element, no prop needed: */}
<button mousemagnetic="">Hover me</button>
Text labels ("View", "Drag", …)
Show a label inside the cursor while hovering an element — the agency-site classic:
<SmartMouse textStyle={{ fontSize: '14px', fontWeight: 600 }} />
<div className="project-card" mouselabel="View"
mousecustom={JSON.stringify({ width: '70px', height: '70px' })}>
...
</div>
Fully custom cursor
Pass children to replace the built-in dot with anything — the follower still tracks, hides, and click-scales for you:
<SmartMouse clickScale={0.8}>
<div style={{ width: 24, height: 24, border: '2px solid black', borderRadius: '50%' }} />
</SmartMouse>
Glass cursor
One prop turns the cursor into frosted glass — the backdrop blurs underneath it and all element colors render as translucent tints:
<SmartMouse glass />
Magnify images
Mark any image and the cursor becomes a circular zoom lens while hovering it:
<SmartMouse magnifyZoom={2.5} magnifySize={140} />
<img src="/photo.jpg" data-mousemagnify="" alt="Zoom me" />
Checkbox pulse
Enabled by default — toggling any checkbox or radio makes the cursor pulse. Turn it off with:
<SmartMouse checkAnimation={false} />
Style any element by CSS selector
selectorStyle matches the hovered element (or any ancestor) against arbitrary CSS selectors — a superset of classStyle:
<SmartMouse
selectorStyle={{
'nav a': { width: '20px', height: '20px', color: '#0af' },
'[data-magnify]': { width: '80px', height: '80px', color: 'rgba(0,0,0,0.2)' },
}}
/>
TypeScript note: custom JSX attributes like
mouselabelmay upset strict JSX typing. Every attribute also works with adata-prefix:data-mouselabel,data-mousecustom,data-mousemagnetic,data-mousemagnify.
Next.js
SmartMouse ships with the "use client" directive built into the bundle, so with the App Router you can import it directly in a Server Component such as app/layout.tsx — no wrapper component needed:
// app/layout.tsx
import SmartMouse from 'react-smart-mouse'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<SmartMouse />
</body>
</html>
)
}
Works with the Pages Router too — just render it in _app.tsx. Tested with Next.js 13 → 16.
Props
| prop | type | default | description |
|---|---|---|---|
| pStyle | object | { color: 'rgba(255, 0, 0, 0.5)', height: '35px', width: '35px', top: '-5px', left: '-10px' } |
Style over p elements. |
| aStyle | object | { color: 'rgba(0, 128, 0, 1)', height: '15px', width: '15px', top: '-5px', left: '-10px' } |
Style over a elements. |
| buttonStyle | object | { color: 'rgba(248, 246, 15, 1)', height: '15px', width: '15px', top: '-5px', left: '-10px' } |
Style over button elements. |
| spanStyle | object | { color: 'rgba(255, 165, 0, 0.5)', height: '35px', width: '35px', top: '-5px', left: '-10px' } |
Style over span elements. |
| imgStyle | object | { color: 'rgba(248, 246, 15, 0.7)', height: '15px', width: '15px', top: '-5px', left: '-10px' } |
Style over img elements. |
| inputStyle | object | { color: 'rgba(67, 39, 245, 1)', height: '30px', width: '2px', top: '-15px', left: '0px' } |
Style over input elements (text caret look). |
| textareaStyle | object | { color: 'rgba(227, 104, 133, 1)', height: '30px', width: '3px', top: '-15px', left: '0px' } |
Style over textarea elements. |
| labelStyle | object | { color: 'rgba(128, 128, 0, 0.5)', height: '35px', width: '35px', top: '-5px', left: '-10px' } |
Style over label elements. |
| liStyle | object | { color: 'rgba(204, 147, 198, 0.5)', height: '35px', width: '35px', top: '-5px', left: '-10px' } |
Style over li elements. |
| checkboxStyle | object | { color: 'rgba(129, 242, 63, 1)', height: '10px', width: '10px', top: '-5px', left: '-5px' } |
Style over checkboxes. |
| radioButtonStyle | object | { color: 'rgba(150, 63, 242, 1)', height: '10px', width: '10px', top: '-5px', left: '-5px' } |
Style over radio buttons. |
| rangeStyle | object | { color: 'rgba(67, 39, 245, 0.8)', height: '18px', width: '18px', top: '-9px', left: '-9px' } |
Style over range sliders. New in 2.2 |
| classStyle | object | none | Styles keyed by className: { [className]: ElementStyle }. |
| selectorStyle | object | none | Styles keyed by any CSS selector, matched with closest(). New in 2.1 |
| defaultWidth | string | 35px |
Default follower width. |
| defaultHeight | string | 35px |
Default follower height. |
| defaultTop | number | string | -5 |
Default top offset of the follower. |
| defaultLeft | number | string | -10 |
Default left offset of the follower. |
| defaultColor | string | rgba(0, 0, 0, 0.5) |
Default follower color. |
| zIndex | number | 3000 |
z-index of the follower. New in 2.1 |
| transitionDuration | string | 0.7s |
CSS duration for size/color morphs. New in 2.1 |
| clickScale | number | 1 |
Scale applied while the mouse button is pressed, e.g. 0.8. New in 2.1 |
| blendMode | string | none | CSS mix-blend-mode for the follower, e.g. 'difference'. New in 2.1 |
| hideOnLeave | boolean | true |
Hide the follower when the cursor leaves the window. New in 2.1 |
| lerp | number | 1 |
Smoothing factor in (0, 1]. Lower = more trailing lag, e.g. 0.15. New in 2.1 |
| magneticSelector | string | none | CSS selector for magnetic elements — follower snaps to their center. New in 2.1 |
| textStyle | object | { fontSize: '12px', color: '#fff' } |
Style for mouselabel text (any CSSProperties). New in 2.1 |
| respectReducedMotion | boolean | true |
Disable animations when the OS requests reduced motion. New in 2.1 |
| checkAnimation | boolean | true |
Pulse the cursor when a checkbox/radio is toggled. New in 2.2 |
| glass | boolean | false |
Frosted-glass cursor: blurred backdrop, translucent colors. New in 2.2 |
| magnifyZoom | number | 2 |
Zoom factor for mousemagnify images. New in 2.2 |
| magnifySize | number | 120 |
Lens diameter (px) for the image magnifier. New in 2.2 |
| children | ReactNode | none | Custom cursor content rendered instead of the built-in dot. New in 2.1 |
| debug | boolean | false |
Keep the native cursor visible and log hovered elements to the console. |
interface ElementStyle {
width?: string
height?: string
color?: string
top?: number | string
left?: number | string
}
Style priority
When several rules could apply to the hovered element, the follower resolves them in this order:
mousecustomattribute on the element (or an ancestor)selectorStylematch on the element or an ancestorclassStylematch on the element's className- Per-tag style (
buttonStyle,aStyle, …) - Defaults (
defaultWidth,defaultColor, …)
Per-element overrides with mousecustom
Give any single element its own cursor style with the mousecustom attribute (also matches when hovering the element's children, and works as data-mousecustom):
<input type="text" mousecustom={JSON.stringify({ height: '50px', color: '#e1e89e' })} />
Touch devices & accessibility
On devices without a fine pointer (phones, tablets) the component renders nothing and leaves the native behavior untouched — no configuration needed.
When the OS reports prefers-reduced-motion: reduce, all transitions and trailing are disabled while the cursor stays fully functional. Opt out with respectReducedMotion={false}.
TypeScript
Full types are shipped with the package:
import SmartMouse, { SmartMouseProps, ElementStyle } from 'react-smart-mouse'
License
MIT Vadym Bilan. See LICENSE.