Docs

Text Icon Effect

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

Installation

Install dependencies

npm install framer-motion lucide-react

Run the following command

It will create a new file text-icon-effect.tsx inside the components/mage-ui/cursor-effects directory.

mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/text-icon-effect.tsx

Paste the code

Open the newly created file and paste the following code:

'use client';
 
import React, { type RefObject, useLayoutEffect, useRef, useState } from 'react';
import { Edit, Search, Play, Link } from 'lucide-react';
 
// Mouse state interface
interface MouseState {
  x: number | null;
  y: number | null;
  elementX: number | null;
  elementY: number | null;
  elementPositionX: number | null;
  elementPositionY: number | null;
}
 
// Custom hook for tracking mouse position
function useMouse(): [MouseState, RefObject<HTMLDivElement>] {
  const [state, setState] = useState<MouseState>({
    x: null,
    y: null,
    elementX: null,
    elementY: null,
    elementPositionX: null,
    elementPositionY: null,
  });
 
  const ref = useRef<HTMLDivElement | null>(null);
 
  useLayoutEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      const newState: Partial<MouseState> = {
        x: event.pageX,
        y: event.pageY,
      };
 
      if (ref.current instanceof Element) {
        const { left, top } = ref.current.getBoundingClientRect();
        const elementPositionX = left + window.scrollX;
        const elementPositionY = top + window.scrollY;
        const elementX = event.pageX - elementPositionX;
        const elementY = event.pageY - elementPositionY;
 
        newState.elementX = elementX;
        newState.elementY = elementY;
        newState.elementPositionX = elementPositionX;
        newState.elementPositionY = elementPositionY;
      }
 
      setState((s) => ({
        ...s,
        ...newState,
      }));
    };
 
    document.addEventListener('mousemove', handleMouseMove);
 
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);
 
  return [state, ref];
}
 
// Icon type definition
type IconType = 'edit' | 'search' | 'play' | 'link';
 
// TextIconCursor component
const TextIconCursor = () => {
  const [mouseState, ref] = useMouse();
  const [cursorContent, setCursorContent] = useState<string | IconType | null>(null);
 
  const icons = {
    edit: <Edit size={16} />,
    search: <Search size={16} />,
    play: <Play size={16} />,
    link: <Link size={16} />,
  };
 
  return (
    <div className='relative w-full h-full' ref={ref}>
      {mouseState.x !== null && mouseState.y !== null && (
        <div
          className='fixed pointer-events-none z-50'
          style={{
            left: mouseState.x,
            top: mouseState.y,
            transform: 'translate(-50%, -50%)',
          }}
        >
          {/* Main cursor */}
          <div className='w-4 h-4 bg-white rounded-full mix-blend-screen' />
 
          {/* Text/Icon container */}
          {cursorContent && (
            <div
              className='absolute left-6 top-0 bg-white/90 text-gray-900 px-3 py-1.5 rounded-full whitespace-nowrap flex items-center gap-2 text-sm animate-fade-in'
              style={{
                animation: 'fadeIn 0.2s ease-out',
              }}
            >
              {typeof cursorContent === 'string' && !Object.keys(icons).includes(cursorContent)
                ? cursorContent
                : icons[cursorContent as IconType]}
            </div>
          )}
        </div>
      )}
 
      <div className='flex flex-col items-center justify-center h-full gap-6'>
        <button
          className='px-6 py-3 bg-white/10 text-white rounded-lg transition-colors'
          onMouseEnter={() => setCursorContent('edit')}
          onMouseLeave={() => setCursorContent(null)}
        >
          Edit Button
        </button>
 
        <div
          className='px-6 py-3 bg-white/10 text-white rounded-lg cursor-help'
          onMouseEnter={() => setCursorContent('Click to search')}
          onMouseLeave={() => setCursorContent(null)}
        >
          Search Area
        </div>
 
        <a
          href='#'
          className='px-6 py-3 bg-white/10 text-white rounded-lg'
          onMouseEnter={() => setCursorContent('link')}
          onMouseLeave={() => setCursorContent(null)}
        >
          Click to Navigate
        </a>
 
        <div
          className='w-32 h-32 bg-white/10 rounded-lg flex items-center justify-center'
          onMouseEnter={() => setCursorContent('play')}
          onMouseLeave={() => setCursorContent(null)}
        >
          <span className='text-white'>Video Area</span>
        </div>
      </div>
 
      <style jsx>{`
        @keyframes fadeIn {
          from {
            opacity: 0;
            transform: translateX(-10px);
          }
          to {
            opacity: 1;
            transform: translateX(0);
          }
        }
      `}</style>
    </div>
  );
};
 
export default TextIconCursor;