Random UI

Mouse Follow

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

Tailwind CSS
Tailwind CSS
React
React
Radix UI
Radix UI

Demo

Mouse Follow

ProjectsHover this card to see the mouse follow effect
AboutIt works smoothly even with multiple items

Custom elements

IconsYou can actually place any element you want inside the MouseFollowItem
DynamicYou can play with it to create funny effects like this (Enter and leave the card more than once)

Different positions

You can use the offsetX and offsetY props to position the item where you want.

CenterThis items is centered to the mouse cursor
Top left
This item is positioned at the top left of the mouse cursor

Installation

Install the following dependencies:

npm install @radix-ui/react-slot
pnpm add @radix-ui/react-slot
yarn add @radix-ui/react-slot
bun add @radix-ui/react-slot

Copy and paste the following code into your project.

mouse-follow.tsx
"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

page.tsx
<MouseFollowContent asChild>
  <div className="my-card-container">
    <span>Card content</span>
    <MouseFollowItem>
        <MyCustomFollowCursorComponent>
    </MouseFollowItem>
  </div>
</MouseFollowContent>

Custom position

page.tsx
<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.

layout.tsx
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

PropTypeDefault ValueDescription
asChildbooleanfalseWhether to render the component as a child
className?string-Additional classes for the component

MouseFollowItem

PropTypeDefault ValueDescription
offsetX?number0The horizontal offset of the item
offsetY?number0The vertical offset of the item
className?string-Additional classes for the item