A lightweight and highly customizable React component for creating unique and interactive cursor experiences with full SSR support.
- ✅ SSR Compatible - Works seamlessly with Next.js, Remix, Gatsby, and other SSR frameworks
- ✅ TypeScript Support - Full type safety out of the box
- ✅ Customizable - Define your own cursor variants and styles
- ✅ Smooth Animations - Powered by Framer Motion
- ✅ Zero Config - Works out of the box with sensible defaults
- ✅ Accessibility - Automatically hides on touch devices and respects user preferences
- ✅ Lightweight - Minimal bundle size impact
npm install @phazr/custom-cursor motion
# or
yarn add @phazr/custom-cursor motion
# or
pnpm add @phazr/custom-cursor motion
import { CursorProvider, Cursor } from '@phazr/custom-cursor';
// Make sure import css also unless its wont work as expected
import '@phazr/custom-cursor/cursor.css';
function App() {
return (
<CursorProvider>
<div>Your app content</div>
<Cursor />
</CursorProvider>
);
}
// app/layout.tsx
import { CursorProvider, Cursor } from '@phazr/custom-cursor';
// Make sure import css also unless its wont work as expected
import '@phazr/custom-cursor/cursor.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<CursorProvider>
{children}
<Cursor />
</CursorProvider>
</body>
</html>
);
}
// pages/_app.tsx
import { CursorProvider, Cursor } from '@phazr/custom-cursor';
// Make sure import css also unless its wont work as expected
import '@phazr/custom-cursor/cursor.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<CursorProvider>
<Component {...pageProps} />
<Cursor />
</CursorProvider>
);
}
import { useCursor } from '@phazr/custom-cursor';
function InteractiveElements() {
const { setVariant } = useCursor();
return (
<div>
<a
href="#"
onMouseEnter={() => setVariant('link')}
onMouseLeave={() => setVariant('default')}
>
Hover me for link cursor!
</a>
<p
onMouseEnter={() => setVariant('text')}
onMouseLeave={() => setVariant('default')}
>
Hover me for text cursor!
</p>
<input
type="text"
onFocus={() => setVariant('input')}
onBlur={() => setVariant('default')}
placeholder="Input field"
/>
<button
onMouseEnter={() => setVariant('sayHi')}
onMouseLeave={() => setVariant('default')}
>
Say Hi Button
</button>
</div>
);
}
import { useCursor } from '@phazr/custom-cursor';
function CustomCursorExample() {
const { setVariant, setCustomConfig } = useCursor();
const handleCustomCursor = () => {
setCustomConfig({
size: 60,
backgroundColor: '#ff0000',
mixBlendMode: 'normal',
text: 'Click',
textColor: '#ffffff',
fontSize: '14px',
fontFamily: 'Arial, sans-serif',
});
setVariant('custom');
};
return (
<div
onMouseEnter={handleCustomCursor}
onMouseLeave={() => setVariant('default')}
style={{ padding: '20px', backgroundColor: '#f0f0f0' }}
>
Hover for custom cursor with text!
</div>
);
}
import { Cursor } from '@phazr/custom-cursor';
// Make sure import css also unless its wont work as expected
import '@phazr/custom-cursor/cursor.css';
function App() {
return (
<CursorProvider>
<div>Your app content</div>
<Cursor
springConfig={{
damping: 20,
stiffness: 300,
}}
/>
</CursorProvider>
);
}
import { CursorProvider } from '@phazr/custom-cursor';
// Make sure import css also unless its wont work as expected
import '@phazr/custom-cursor/cursor.css';
function App() {
return (
<CursorProvider enableOnTouch={true}>
<div>Your app content</div>
<Cursor />
</CursorProvider>
);
}
The main provider component that should wrap your application.
interface CursorProviderProps {
children: ReactNode;
className?: string;
enableOnTouch?: boolean; // Enable cursor on touch devices (default: false)
}
The cursor component that renders the actual cursor.
interface CursorProps {
className?: string;
springConfig?: {
damping?: number; // Default: 28
stiffness?: number; // Default: 500
};
}
Hook to control cursor variants and configuration.
interface CursorContextType {
variant: CursorVariant;
setVariant: (variant: CursorVariant) => void;
customConfig?: CustomCursorConfig;
setCustomConfig?: (config: CustomCursorConfig) => void;
}
const { variant, setVariant, customConfig, setCustomConfig } = useCursor();
type CursorVariant = 'default' | 'link' | 'text' | 'input' | 'sayHi' | 'custom';
interface CustomCursorConfig {
size?: number;
backgroundColor?: string;
mixBlendMode?: string;
text?: string;
textColor?: string;
fontSize?: string;
fontFamily?: string;
}
default
- Standard cursor (16x16px, white, difference blend mode)link
- Larger cursor for links (64x64px, white, difference blend mode)text
- Smaller cursor for text (8x8px, white, difference blend mode)input
- Hidden cursor for inputs (invisible)sayHi
- Special cursor with "Say Hi" text (90x90px)custom
- Fully customizable cursor usingsetCustomConfig()
import { useCursor } from '@phazr/custom-cursor';
function Portfolio() {
const { setVariant, setCustomConfig } = useCursor();
const projects = [
{ id: 1, title: 'Project A', action: 'View' },
{ id: 2, title: 'Project B', action: 'Explore' },
{ id: 3, title: 'Project C', action: 'Discover' },
];
const handleProjectHover = (action: string) => {
setCustomConfig({
size: 80,
backgroundColor: '#000000',
text: action,
textColor: '#ffffff',
fontSize: '16px',
mixBlendMode: 'normal',
});
setVariant('custom');
};
return (
<div className="portfolio">
{projects.map((project) => (
<div
key={project.id}
onMouseEnter={() => handleProjectHover(project.action)}
onMouseLeave={() => setVariant('default')}
className="project-card"
>
<h3>{project.title}</h3>
</div>
))}
</div>
);
}
The package includes default styles that you can override:
/* Override cursor container styles */
.phazr-cursor-container {
/* Your custom styles */
}
/* Override cursor text styles */
.phazr-cursor-text {
/* Your custom styles */
}
/* Override "Say Hi" variant styles */
.phazr-cursor-sayhi {
/* Your custom styles */
}
- Make sure you've wrapped your app with
CursorProvider
- Ensure you've included the
<Cursor />
component - Check that you're not on a touch device (cursor is hidden by default)
If you experience performance issues:
- Adjust the spring configuration to reduce stiffness
- Limit the frequency of variant changes
- Use
React.memo
for components that frequently change cursor variants
The package automatically handles SSR by:
- Detecting touch devices server-side and client-side
- Hiding cursor on touch devices by default
- Using proper
useEffect
hooks for client-side only code
By default, the cursor is hidden on touch devices. To enable it:
<CursorProvider enableOnTouch={true}>
<YourApp />
</CursorProvider>
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
Contributions are welcome! Please feel free to submit a Pull Request. See CONTRIBUTORS.md
for a list of contributors.
MIT © Phazr Inc
For more information, visit the GitHub repository.