Image Mousetrail

An image with a mouse trail effect, where the image responds dynamically to cursor movement, creating a visually engaging interaction.

image-0image-1image-2image-3image-4image-5image-6image-7image-8

✨ Experience Interactive Designs with
Dynamic Mouse Trails

Usage

mousetrail.tsx
//@ts-nocheck
'use client'
import { cn } from '@/lib/utils'
import { createRef, useRef } from 'react'
interface ImageMouseTrailProps {
  items: any[]
  children?: ReactNode
  className?: string
  imgClass?: string
  distance?: number
  maxNumberOfImages?: number
  fadeAnimation?: boolean
}
export default function ImageMouseTrail({
  items,
  children,
  className,
  maxNumberOfImages = 5,
  imgClass = 'w-40 h-48',
  distance = 20,
  fadeAnimation = false,
}: ImageMouseTrailProps) {
  const containerRef = useRef(null)
  const refs = useRef(items.map(() => createRef<HTMLImageElement>()))
  const currentZIndexRef = useRef(1)
 
  let globalIndex = 0
  let last = { x: 0, y: 0 }
 
  const activate = (image, x, y) => {
    const containerRect = containerRef.current?.getBoundingClientRect()
    const relativeX = x - containerRect.left
    const relativeY = y - containerRect.top
    image.style.left = `${relativeX}px`
    image.style.top = `${relativeY}px`
    console.log(refs.current[refs.current?.length - 1])
 
    if (currentZIndexRef.current > 40) {
      currentZIndexRef.current = 1
    }
    image.style.zIndex = String(currentZIndexRef.current)
    currentZIndexRef.current++
 
    image.dataset.status = 'active'
    if (fadeAnimation) {
      setTimeout(() => {
        image.dataset.status = 'inactive'
      }, 1500)
    }
    last = { x, y }
  }
 
  const distanceFromLast = (x, y) => {
    return Math.hypot(x - last.x, y - last.y)
  }
  const deactivate = (image) => {
    image.dataset.status = 'inactive'
  }
 
  const handleOnMove = (e) => {
    if (distanceFromLast(e.clientX, e.clientY) > window.innerWidth / distance) {
      console.log(e.clientX, e.clientY)
 
      const lead = refs.current[globalIndex % refs.current.length].current
 
      const tail =
        refs.current[(globalIndex - maxNumberOfImages) % refs.current.length]
          ?.current
 
      if (lead) activate(lead, e.clientX, e.clientY)
      if (tail) deactivate(tail)
      globalIndex++
    }
  }
 
  return (
    <section
      onMouseMove={handleOnMove}
      onTouchMove={(e) => handleOnMove(e.touches[0])}
      ref={containerRef}
      className={cn(
        'grid place-content-center h-[600px] w-full bg-[#e0dfdf] relative overflow-hidden rounded-lg',
        className
      )}
    >
      {items.map((item, index) => (
        <>
          <img
            key={index}
            className={cn(
              "object-cover  scale-0 opacity:0 data-[status='active']:scale-100  data-[status='active']:opacity-100 transition-transform data-[status='active']:duration-500 duration-300 data-[status='active']:ease-out-expo  absolute   -translate-y-[50%] -translate-x-[50%] ",
              imgClass
            )}
            data-index={index}
            data-status="inactive"
            src={item}
            alt={`image-${index}`}
            ref={refs.current[index]}
          />
        </>
      ))}
      <article className="relative z-50 mix-blend-difference">
        {children}
      </article>
    </section>
  )
}

I use data-status atribute to handle active and inactive item and you can add animation using this attribute:

{
  items.map((item, index) => (
    <>
      <img
        key={index}
        className={cn(
          "object-cover  scale-0 opacity:0 data-[status='active']:scale-100  data-[status='active']:opacity-100 transition-transform data-[status='active']:duration-500 duration-300 data-[status='active']:ease-out-expo  absolute   -translate-y-[50%] -translate-x-[50%] ",
          imgClass
        )}
        data-index={index}
        data-status="inactive"
        src={item}
        alt={`image-${index}`}
        ref={refs.current[index]}
      />
    </>
  ))
}

Add the following cubic-bazeir to your tailwind.config.js file:

 transitionTimingFunction: {
  'out-expo': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
 },

you can fade out animation when you don't mousemove:

const activate = (image, x, y) => {
  setTimeout(() => {
    image.dataset.status = 'inactive'
  }, 1000)
}

Props

PropTypeDefaultDescription
itemsany[]-Array of items to display in the mouse trail effect.
childrenReactNodeundefinedOptional child elements to render inside the component.
classNamestringundefinedOptional CSS class for styling the wrapper of the component.
imgClassstring'w-40 h-48'CSS class for styling the images in the mouse trail.
distancenumber20Distance between each image in the mouse trail.
maxNumberOfImagesnumber5Maximum number of images to display in the trail.
fadeAnimationbooleanfalseWhether to apply fade animation to the images.

Example

Small Images

image-0image-1image-2image-3image-4image-5image-6image-7image-8image-9image-10image-11

✨Experience

Disappear Images

image-0image-1image-2image-3image-4image-5image-6image-7image-8

✨ Experience Interactive Designs with
Dynamic Mouse Trails

Without Components

image-0image-1image-2image-3image-4image-5image-6image-7image-8image-9image-10image-11image-12image-13image-14image-15

✨ Experience Interactive Designs
with Dynamic Mouse Trails
built with Tailwind CSS