Mouse Follow
A performant and customisable follow mouse component built with React compound components


Demo
Mouse Follow
Custom elements
Different positions
You can use the offsetX and offsetY props to position the item where you want.
Installation
Install the following dependencies:
npm install @radix-ui/react-slotpnpm add @radix-ui/react-slotyarn add @radix-ui/react-slotbun add @radix-ui/react-slotCopy and paste the following code into your project.
"use client";
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
interface MouseContextType {
isVisible: boolean;
x: number;
y: number;
}
const MouseContext = React.createContext<MouseContextType | null>(null);
function MouseFollowContent({
asChild,
className,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const [mouseState, setMouseState] = React.useState<MouseContextType>({
isVisible: false,
x: 0,
y: 0,
});
const handleMouseEnter = React.useCallback(() => {
setMouseState((prev) => ({ ...prev, isVisible: true }));
}, []);
const handleMouseLeave = React.useCallback(() => {
setMouseState((prev) => ({ ...prev, isVisible: false }));
}, []);
const handleMouseMove = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
setMouseState({
isVisible: true,
x: e.clientX,
y: e.clientY,
});
},
[]
);
const Comp = asChild ? Slot : "div";
return (
<MouseContext.Provider value={mouseState}>
<Comp
data-slot="mouse-follow-content"
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
{...props}
/>
</MouseContext.Provider>
);
}
MouseFollowContent.displayName = "MouseFollowContent";
interface MouseFollowItemProps extends React.ComponentProps<"div"> {
offsetX?: number;
offsetY?: number;
}
function MouseFollowItem({
offsetX = 0,
offsetY = 0,
className,
...props
}: MouseFollowItemProps) {
const context = React.useContext(MouseContext);
if (!context) {
console.error("MouseFollowItem must be used inside MouseFollowContent");
return null;
}
const { isVisible, x, y } = context;
return (
<div
data-slot="mouse-follow-item"
aria-hidden="true"
className={cn(
"pointer-events-none fixed z-[999]",
isVisible
? "scale-100 transition-transform duration-150"
: "scale-0 duration-0",
className
)}
style={{
left: x + offsetX,
top: y + offsetY,
transform: "translate(-50%, -50%)",
}}
{...props}
/>
)
}
MouseFollowItem.displayName = "MouseFollowItem";
export { MouseFollowContent, MouseFollowItem };Usage
import { MouseFollowContent, MouseFollowItem } from "@/components/mouse-follow";Basic
<MouseFollowContent asChild>
<div className="my-card-container">
<span>Card content</span>
<MouseFollowItem>
<MyCustomFollowCursorComponent>
</MouseFollowItem>
</div>
</MouseFollowContent>Custom position
<MouseFollowContent asChild>
<div className="my-card-container">
<span>Card content</span>
<MouseFollowItem offsetX={40} offsetY={30}>
<div>Follow cursor at the bottom right</div>
</MouseFollowItem>
</div>
</MouseFollowContent>Global
You can also wrap your entire page in the MouseFollowContent to make it follow the mouse cursor everywhere.
export default function Layout({ children }) {
return (
<html>
<MouseFollowContent asChild>
<body className="flex flex-col min-h-svh">
{children}
<MouseFollowItem>
<span>Follow everywhere</span>
</MouseFollowItem>
</body>
</MouseFollowContent>
</html>
);
}Techbook
When working with this comoponent, I faced an interesting use case. When trying to place a 3D model in the MouseFollowItem,
I noticed that, at page load, the model was not visibile, unless the user moved the mouse cursor before the page was fully loaded.
After some try and error, I found that the behaviour was related to the class scale-0 applied to the MouseFollowItem when the mouse cursor is not over its container.
I assume that having the scale set to 0, was somehow breaking the 3D canvas size calculation.
As a workaround, I switched to use the opacity instead of scaling, with opacity-0 instead of scale-0, and it worked.
So this is a good example of how you can adapt the component to your needs!
Props
MouseFollowContent
| Prop | Type | Default Value | Description |
|---|---|---|---|
| asChild | boolean | false | Whether to render the component as a child |
| className? | string | - | Additional classes for the component |
MouseFollowItem
| Prop | Type | Default Value | Description |
|---|---|---|---|
| offsetX? | number | 0 | The horizontal offset of the item |
| offsetY? | number | 0 | The vertical offset of the item |
| className? | string | - | Additional classes for the item |
