Random Access Components

Mouse Follow

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

TailwindCSS iconTailwind CSS
Radix UI iconRadix UI
React IconReact
Next.js IconNext.js

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

Different positions

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

Top left
This item is positioned at the top left of the mouse cursor
"use client";
import { MouseFollowContent, MouseFollowItem } from "@/components/mouse-follow";
import { useState } from "react";
import { Volume2Icon, VolumeOffIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import Image from "next/image";
import tailwindLogo from "@/public/assets/badges/tailwind_logo.png";

const EMOJIS = [
"๐Ÿคก", "๐Ÿค”", "๐Ÿคจ", "๐Ÿค“", "๐Ÿคฏ", "๐Ÿค ","๐Ÿ‘น", 
"๐Ÿ’€", "๐Ÿ‘บ", "๐Ÿ‘ป", "๐Ÿ‘ฝ", "๐Ÿ‘พ", "๐Ÿค–", "๐ŸŽƒ", 
"๐Ÿ‘ฟ", "๐Ÿ’ฉ", "๐Ÿ‘", "๐Ÿ‘‹", "๐Ÿ‘Œ", "๐Ÿ‘", "๐Ÿ‘Ž", 
"๐Ÿ‘Š", "๐Ÿ‘", "๐Ÿ‘‹", "๐Ÿ‘Œ", "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘Š"
];

const FollowMouseDemo: React.FC = () => {

const [isVolumeOn, setIsVolumeOn] = useState(false);

const [selectedEmoji, setSelectedEmoji] = useState(EMOJIS[Math.floor(Math.random() * EMOJIS.length)]);

const onMouseLeave = () => {
    setSelectedEmoji(EMOJIS[Math.floor(Math.random() * EMOJIS.length)]);
}

return ( 
  <div className="flex flex-col w-full items-center justify-around space-y-20">
    <h2 className="mt-0 text-4xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">
        Mouse Follow
    </h2>
    <div className="w-full gap-4 flex justify-center">
        <MouseFollowContent asChild>
            <div className="flex cursor-pointer flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <span className="text-base text-primary">
                    Projects
                </span>
                <span className="text-sm text-muted-foreground">
                    Hover this card to see the mouse follow effect
                </span>
                <MouseFollowItem offsetX={50} offsetY={30}>
                    <FollowMouseItemContent label="Discover" />
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
        <MouseFollowContent asChild>
            <div className="flex cursor-pointer flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <span className="text-base text-primary">
                    About
                </span>
                <span className="text-sm text-muted-foreground">
                    It works smoothly even with multiple items
                </span>
                <MouseFollowItem offsetX={50} offsetY={30}>
                    <FollowMouseItemContent label="See more" />
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
    </div>
    <h2 className="mt-0 text-4xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">
        Custom elements
    </h2>
    <div className="w-full gap-4 flex justify-center">
        <MouseFollowContent asChild>
            <div className="flex cursor-pointer flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <span className="text-base text-primary">
                    Icons
                </span>
                <span className="text-sm text-muted-foreground">
                    You can actually place any element you want inside the MouseFollowItem
                </span>
                <MouseFollowItem offsetX={50} offsetY={30}>
                    <div className="flex items-center gap-2 relative overflow-hidden rounded-full size-10 p-2">
                        <Image src={tailwindLogo} alt="Tailwind CSS" fill className="object-contain !m-0" />
                    </div>
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
        <MouseFollowContent asChild>
            <div
            onMouseLeave={onMouseLeave}
            className="flex cursor-pointer flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <span className="text-base text-primary">
                    Dynamic
                </span>
                <span className="text-sm text-muted-foreground">
                    You can play with it to create funny effects like this (Enter and leave the card more than once)
                </span>
                <MouseFollowItem offsetX={25} offsetY={25}>
                    {selectedEmoji}
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
    </div>
    <h2 className="mt-0 mb-2 text-4xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">
        Different positions
    </h2>
    <p className="text-sm text-muted-foreground text-center">
        You can use the offsetX and offsetY props to position the item where you want.
    </p>
    <div className="w-full gap-4 flex justify-center">
        <MouseFollowContent asChild>
            <div onClick={() => setIsVolumeOn(!isVolumeOn)} className="flex cursor-none select-none flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <span className="text-base text-primary">
                    Center
                </span>
                <span className="text-sm text-muted-foreground">
                    This items is centered to the mouse cursor
                </span>
                <MouseFollowItem>
                    <CenterItemContent isVolumeOn={isVolumeOn} />
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
        <MouseFollowContent asChild>
            <div className="flex flex-col w-60 gap-4 bg-gradient-to-br from-background to-muted-foreground/10 border border-muted-foreground/10 rounded-lg p-4">
                <div className="flex flex-col gap-2">
                    <span className="text-base text-primary">
                        Top left
                    </span>
                </div>
                <span className="text-sm text-muted-foreground"> This item is positioned at the top left of the mouse cursor</span>
                <MouseFollowItem offsetX={-40} offsetY={-20}>
                    <FollowMouseItemContent label="Click me" />
                </MouseFollowItem>
            </div>
        </MouseFollowContent>
    </div>
  </div>
);
}

const FollowMouseItemContent = ({ label }: { label: string }) => {
return (
    <div className="rounded-lg text-xs bg-muted-foreground/10 backdrop-blur-sm border border-muted-foreground/10 py-1 px-2">
  <div className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">{label}</div>
</div>
);
};

const CenterItemContent = ({ isVolumeOn }: { isVolumeOn: boolean }) => {
return (
    <div className={cn("rounded-full aspect-square text-xs p-6 border-2 border-dashed border-muted-foreground/20 transition-all duration-150", isVolumeOn && "border-primary/50 text-primary")}>
        <Volume2Icon className={cn("size-4 text-primary transition-all duration-150 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", !isVolumeOn && "scale-0 opacity-0")} />
        <VolumeOffIcon className={cn("size-4 text-primary transition-all duration-150 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", isVolumeOn && "scale-0 opacity-0")} />
</div>
);
};

Installation

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

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

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

On this page