Docs

Textflag Cursor 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 textflag-cursor-effect.tsx inside the components/mage-ui/cursor-effects directory.

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

Paste the code

Open the newly created file and paste the following code:

'use client';
 
import { useTheme } from 'next-themes';
import React, { useEffect, useRef } from 'react';
 
interface TextFlagOptions {
  text?: string;
  color?: string;
  font?: string;
  textSize?: number;
  gap?: number;
}
 
const TextFlagCursor: React.FC<TextFlagOptions> = ({
  text = 'Hello World',
  color = '#000000',
  font = 'monospace',
  textSize = 12,
  gap = textSize + 2,
}) => {
  const cursorRef = useRef<{ destroy: () => void } | null>(null);
 
  useEffect(() => {
    const element = document.body;
    let width = window.innerWidth;
    let height = window.innerHeight;
    let cursor = { x: width / 2, y: height / 2 };
    let charArray = text.split('').map((letter) => ({ letter, x: width / 2, y: height / 2 }));
    let angle = 0;
    let radiusX = 2;
    let radiusY = 5;
 
    let canvas = document.createElement('canvas');
    let context = canvas.getContext('2d');
    if (!context) return;
 
    canvas.style.position = 'fixed';
    canvas.style.top = '0';
    canvas.style.left = '0';
    canvas.style.pointerEvents = 'none';
    document.body.appendChild(canvas);
    canvas.width = width;
    canvas.height = height;
 
    const onMouseMove = (e: MouseEvent) => {
      cursor.x = e.clientX;
      cursor.y = e.clientY;
    };
 
    const onWindowResize = () => {
      width = window.innerWidth;
      height = window.innerHeight;
      canvas.width = width;
      canvas.height = height;
    };
 
    const updateParticles = () => {
      context.clearRect(0, 0, width, height);
      angle += 0.15;
      let locX = radiusX * Math.cos(angle);
      let locY = radiusY * Math.sin(angle);
 
      for (let i = charArray.length - 1; i > 0; i--) {
        charArray[i].x = charArray[i - 1].x + gap;
        charArray[i].y = charArray[i - 1].y;
        context.fillStyle = color;
        context.font = `${textSize}px ${font}`;
        context.fillText(charArray[i].letter, charArray[i].x, charArray[i].y);
      }
 
      charArray[0].x += (cursor.x - charArray[0].x) / 5 + locX + 2;
      charArray[0].y += (cursor.y - charArray[0].y) / 5 + locY;
    };
 
    const loop = () => {
      updateParticles();
      requestAnimationFrame(loop);
    };
 
    element.addEventListener('mousemove', onMouseMove);
    window.addEventListener('resize', onWindowResize);
    loop();
 
    cursorRef.current = {
      destroy: () => {
        canvas.remove();
        element.removeEventListener('mousemove', onMouseMove);
        window.removeEventListener('resize', onWindowResize);
      },
    };
 
    return () => cursorRef.current?.destroy();
  }, [text, color, font, textSize, gap]);
 
  return null;
};
 
const Page = () => {
  const { theme } = useTheme();
  const textColor = theme === 'dark' ? '#000000' : '#FFFFFF';
 
  return (
    <div className="h-screen">
      <TextFlagCursor text="IMage UI" color={textColor} font="monospace" textSize={12} />
    </div>
  );
};
 
export default Page;