Light Leak
Light Leak

Cursor Follow

A fully customizable, accessible and versatile cursor follow component with 5 pre-built styles and animations. Easily create cool-looking cursor/mouse following effect on any of your projects.

Introduction

The ZyfloCursor component is a versatile and customizable cursor that can be easily integrated into your Next.js project. It offers a range of styling options and animations to enhance the user experience.

Add The Component

Add the following component to your project in the /components/zyflo directory:

"use client"

import React, { useEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
import { motion, useSpring, useMotionValue } from "framer-motion"
import { cva, type VariantProps } from "class-variance-authority"

export const PossibleZyfloCursorVariant = [
  "default",
  "spot-blur",
  "dot",
  "ring",
  "inverted"
] as const

export type ZyfloCursorVariant = (typeof PossibleZyfloCursorVariant)[number]

const cursorVariants = cva("absolute", {
  variants: {
    variant: {
      default: "size-8 rounded-full border-2 border-primary",
      "spot-blur": "size-16 rounded-full bg-primary/30 blur-xl",
      dot: "size-3 rounded-full bg-primary",
      ring: "size-8 rounded-full border-2 border-primary",
      inverted: "size-8 rounded-full bg-foreground mix-blend-difference"
    }
  },
  defaultVariants: { variant: "default" }
})

interface ZyfloCursorProps extends VariantProps<typeof cursorVariants> {
  containerRef: React.RefObject<HTMLElement>
  delay?: number
  easing?: string
  srOnly?: string
  label?: string
  color?: string
}

const useHoverState = () => {
  const [isHovering, setIsHovering] = useState(false)

  const onMouseEnter = (e: MouseEvent) => {
    const target = e.target as HTMLElement
    if (
      target.tagName === "A" ||
      target.classList.contains("zyflo-hover") ||
      target.closest("a, .zyflo-hover")
    ) {
      setIsHovering(true)
    }
  }

  const onMouseLeave = (e: MouseEvent) => {
    const target = e.target as HTMLElement
    if (
      target.tagName === "A" ||
      target.classList.contains("zyflo-hover") ||
      target.closest("a, .zyflo-hover")
    ) {
      setIsHovering(false)
    }
  }

  return { isHovering, onMouseEnter, onMouseLeave }
}

const ZyfloCursor: React.FC<ZyfloCursorProps> = ({
  containerRef,
  variant = "default",
  delay = 0.1,
  easing = "spring(100, 10, 0, 0)",
  srOnly,
  label,
  color
}) => {
  const cursorRef = useRef<HTMLDivElement>(null)
  const mouseX = useMotionValue(0)
  const mouseY = useMotionValue(0)
  const { isHovering, onMouseEnter, onMouseLeave } = useHoverState()

  const springConfig = {
    bounce: 0.5,
    stiffness: 4000,
    damping: 500,
    mass: 10,
    duration: 0.1
  }

  const cursorX = useSpring(mouseX, springConfig)
  const cursorY = useSpring(mouseY, springConfig)

  useEffect(() => {
    const container = containerRef ? containerRef.current : null
    if (!container) return

    const updateCursorPosition = (e: MouseEvent) => {
      const rect = container.getBoundingClientRect()
      mouseX.set(
        e.clientX -
          rect.left -
          (variant === "spot-blur" ? 54 : variant === "dot" ? 37 : 46)
      )
      mouseY.set(
        e.clientY -
          rect.top -
          (variant === "spot-blur" ? 138 : variant === "dot" ? 118 : 126)
      )
    }

    container.addEventListener("mousemove", updateCursorPosition)
    container.addEventListener("mouseenter", onMouseEnter, true)
    container.addEventListener("mouseleave", onMouseLeave, true)

    return () => {
      container.removeEventListener("mousemove", updateCursorPosition)
      container.removeEventListener("mouseenter", onMouseEnter, true)
      container.removeEventListener("mouseleave", onMouseLeave, true)
    }
  }, [containerRef, mouseX, mouseY, onMouseEnter, onMouseLeave, variant])

  const renderCursor = () => {
    const styles = {
      left: cursorX,
      top: cursorY,
      borderColor: color,
      backgroundColor: variant === "spot-blur" ? `${color}4D` : color // 4D is 30% opacity in hex
    }

    const hoverStyles = {
      scale: isHovering ? 1.5 : 1,
      opacity: isHovering ? 0.4 : 1
    }

    const commonProps = {
      className: cursorVariants({ variant }),
      style: styles,
      animate: hoverStyles,
      transition: { delay, ease: easing, ...hoverStyles }
    }

    switch (variant) {
      case "spot-blur":
      case "dot":
      case "ring":
      case "inverted":
        return (
          <div className="relative">
            <motion.div {...commonProps} />
          </div>
        )
      default:
        return (
          <div className="relative">
            <motion.div {...commonProps} />
            <motion.div
              className={`absolute ml-[12px] mt-[12px] size-2 rounded-full !bg-${
                color ? `[${color}]` : "primary"
              }`}
              style={{
                ...styles,
                backgroundColor: color ? color : "hsl(var(--primary))"
              }}
              animate={hoverStyles}
              transition={{ delay, ease: easing, ...hoverStyles }}
            />
          </div>
        )
    }
  }

  return (
    <div
      ref={cursorRef}
      aria-label={label}
      onMouseEnter={
        onMouseEnter as unknown as React.MouseEventHandler<HTMLDivElement>
      }
      onMouseLeave={
        onMouseLeave as unknown as React.MouseEventHandler<HTMLDivElement>
      }
    >
      {renderCursor()}
      {srOnly && <span className="sr-only">{srOnly}</span>}
    </div>
  )
}

export default ZyfloCursor

Usage

Here's an example of how to use the ZyfloCursor component:

Zyflo Window Preview
Spot Blur Cursor

Variants

The ZyfloCursor component supports several variants:

Default

Zyflo Window Preview
Default Cursor

Spot Blur

Zyflo Window Preview
Spot Blur Cursor

Dot

Zyflo Window Preview
Dot Cursor

Ring

Zyflo Window Preview
Ring Cursor

Inverted

Zyflo Window Preview
Inverted Cursor

Props

The ZyfloCursor component accepts the following props:

Quick Props Overview

ZyfloCursor Component Props
PropDescription
containerRefReference to the container element
variantDefines the visual style of the cursor
delayDelay for cursor animation
easingEasing function for cursor animation
srOnlyScreen reader only text
labelAria label for the cursor
colorCustom color for the cursor

Detailed Props Overview

containerRef

  • Type: React.RefObject<HTMLElement>
  • Required: Yes

Reference to the container element where the cursor will be active.

variant

  • Type: ZyfloCursorVariant
  • Default: "default"

Defines the visual style of the cursor. Possible values are:

  • "default"
  • "spot-blur"
  • "dot"
  • "ring"
  • "inverted"
export const PossibleZyfloCursorVariant = [
    "default",
    "spot-blur",
    "dot",
    "ring",
    "inverted"
] as const

export type ZyfloCursorVariant = (typeof PossibleZyfloCursorVariant)[number]

delay

  • Type: number
  • Default: 0.1

Delay for cursor animation in seconds.

easing

  • Type: string
  • Default: "spring(100, 10, 0, 0)"

Easing function for cursor animation.

srOnly

  • Type: string
  • Required: No

Screen reader only text for accessibility.

label

  • Type: string
  • Required: No

Aria label for the cursor.

color

  • Type: string
  • Required: No

Custom color for the cursor.

Customization

The component uses Tailwind CSS classes for styling. You can customize its appearance by modifying the CSS classes in the component's source code or by passing additional classes through the className prop.

Accessibility

The component is designed with accessibility in mind:

  • It uses semantic HTML elements for proper structure.
  • The srOnly prop can be used to add screen reader-only text.
  • The label prop provides an aria-label for the cursor.

Notes

  • The component uses Framer Motion for animations. Make sure you have Framer Motion installed in your project.
  • The cursor's colors and styles are designed to work with both light and dark modes.
  • The zyflo-hover class can be added to elements to trigger the hover effect on the cursor.

Contributing

If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on our GitHub repository. We appreciate your contributions and are always open to collaboration.

Thank you for considering contributing to Zyflo!