DocsCustom Cursor

Custom Cursor

Animated custom cursor component

Users Love Interactions.
Hover over the illustration and you gonna see
Niko
Hi, it's my first page.

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion

Create @/utils/cn.ts file

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Cursor component

'use client'

import React, { useEffect, useState } from 'react'
import { motion, AnimatePresence, useMotionValue } from 'framer-motion'
import { cn } from '@/utils/cn'

export const Pointer = ({
  children,
  className,
  name
}: {
  children: React.ReactNode
  className?: string
  name: string
}) => {
  const x = useMotionValue(0)
  const y = useMotionValue(0)
  const ref = React.useRef<HTMLDivElement>(null)
  const [rect, setRect] = useState<DOMRect | null>(null)
  const [isInside, setIsInside] = useState<boolean>(false)

  useEffect(() => {
    if (ref.current) {
      setRect(ref.current.getBoundingClientRect())
    }
  }, [])

  return (
    <div
      onMouseLeave={() => setIsInside(false)}
      onMouseEnter={() => setIsInside(true)}
      onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
        if (rect) {
          const scrollX = window.scrollX
          const scrollY = window.scrollY
          x.set(e.clientX - rect.left + scrollX)
          y.set(e.clientY - rect.top + scrollY)
        }
      }}
      style={{
        cursor: 'none'
      }}
      ref={ref}
      className={cn('relative', className)}>
      <AnimatePresence>{isInside && <FollowPointer x={x} y={y} name={name} />}</AnimatePresence>
      {children}
    </div>
  )
}

export const FollowPointer = ({ x, y, name }: { x: any; y: any; name: string }) => {
  return (
    <>
      <motion.div
        className="absolute z-50 h-4 w-4 rounded-full"
        style={{
          top: y,
          left: x,
          pointerEvents: 'none'
        }}
        initial={{
          scale: 1,
          opacity: 1
        }}
        animate={{
          scale: 1,
          opacity: 1
        }}
        exit={{
          scale: 0,
          opacity: 0
        }}>
        <svg
          stroke="currentColor"
          fill="currentColor"
          strokeWidth="1"
          viewBox="0 0 16 16"
          className="h-6 w-6 -translate-x-[12px] -translate-y-[10px] -rotate-[70deg] transform stroke-sky-600 text-sky-500"
          height="1em"
          width="1em"
          xmlns="http://www.w3.org/2000/svg">
          <path d="M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103z"></path>
        </svg>
        <div className="w-fit rounded-full bg-sky-500 px-2 py-1 text-white">{name || 'Tony'}</div>
      </motion.div>
    </>
  )
}