Textflag Cursor Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
1'use client';2import { useTheme } from 'next-themes';3import React from 'react';4import TextFlagCursor from './TextFlagCursor';56function index() {7const { theme } = useTheme();89return (10<div>11<TextFlagCursor12text='Hello World'13color={theme === 'dark' ? '#FFFFFF' : '#000000'}14font='monospace'15textSize={12}16/>17</div>18);19}2021export default index;22
1// @ts-nocheck2'use client';34import { useEffect, useRef } from 'react';56interface TextFlagOptions {7text?: string;8color?: string;9font?: string;10textSize?: number;11gap?: number;12element?: HTMLElement;13size?: number;14}1516export const TextFlagCursor: React.FC<TextFlagOptions> = (options) => {17const cursorRef = useRef<{ destroy: () => void } | null>(null);1819useEffect(() => {20let cursorOptions = options || {};21let hasWrapperEl = options && options.element;22let element = hasWrapperEl || document.body;2324let text = cursorOptions.text ? ' ' + options.text : ' Your Text Here';25let color = options?.color || '#000000';26let font = cursorOptions.font || 'monospace';27let textSize = cursorOptions.textSize || 12;28let fontFamily = textSize + 'px ' + font;29let gap = cursorOptions.gap || textSize + 2;30let angle = 0;31let radiusX = 2;32let radiusY = 5;33let charArray = [];3435let width = window.innerWidth;36let height = window.innerHeight;37let cursor = { x: width / 2, y: width / 2 };3839for (let i = 0; i < text.length; i++) {40charArray[i] = { letter: text.charAt(i), x: width / 2, y: width / 2 };41}4243let canvas: HTMLCanvasElement,44context: CanvasRenderingContext2D | null,45animationFrame: number;4647// const size = options?.size || 3;48// let cursorsInitted = false;4950const prefersReducedMotion = window.matchMedia(51'(prefers-reduced-motion: reduce)'52);5354function init() {55if (prefersReducedMotion.matches) {56console.log(57'This browser has prefers reduced motion turned on, so the cursor did not init'58);59return false;60}6162canvas = document.createElement('canvas');63context = canvas.getContext('2d');64canvas.style.top = '0px';65canvas.style.left = '0px';66canvas.style.pointerEvents = 'none';6768if (hasWrapperEl) {69canvas.style.position = 'absolute';70element.appendChild(canvas);71canvas.width = element.clientWidth;72canvas.height = element.clientHeight;73} else {74canvas.style.position = 'fixed';75document.body.appendChild(canvas);76canvas.width = width;77canvas.height = height;78}7980bindEvents();81loop();82}8384function bindEvents() {85element.addEventListener('mousemove', onMouseMove);86window.addEventListener('resize', onWindowResize);87}8889function onWindowResize() {90width = window.innerWidth;91height = window.innerHeight;9293if (hasWrapperEl) {94canvas.width = element.clientWidth;95canvas.height = element.clientHeight;96} else {97canvas.width = width;98canvas.height = height;99}100}101102function onMouseMove(e: MouseEvent) {103if (hasWrapperEl) {104const boundingRect = element.getBoundingClientRect();105cursor.x = e.clientX - boundingRect.left;106cursor.y = e.clientY - boundingRect.top;107} else {108cursor.x = e.clientX;109cursor.y = e.clientY;110}111}112113function updateParticles() {114if (!context) return;115context.clearRect(0, 0, width, height);116117angle += 0.15;118let locX = radiusX * Math.cos(angle);119let locY = radiusY * Math.sin(angle);120121for (let i = charArray.length - 1; i > 0; i--) {122charArray[i].x = charArray[i - 1].x + gap;123charArray[i].y = charArray[i - 1].y;124125context.fillStyle = color;126context.font = fontFamily;127context.fillText(charArray[i].letter, charArray[i].x, charArray[i].y);128}129130let x1 = charArray[0].x;131let y1 = charArray[0].y;132x1 += (cursor.x - x1) / 5 + locX + 2;133y1 += (cursor.y - y1) / 5 + locY;134charArray[0].x = x1;135charArray[0].y = y1;136}137138function loop() {139updateParticles();140animationFrame = requestAnimationFrame(loop);141}142143function destroy() {144canvas.remove();145cancelAnimationFrame(animationFrame);146element.removeEventListener('mousemove', onMouseMove);147window.addEventListener('resize', onWindowResize);148}149150const handleReducedMotionChange = () => {151if (prefersReducedMotion.matches) {152destroy();153} else {154init();155}156};157158prefersReducedMotion.addEventListener('change', handleReducedMotionChange);159init();160161cursorRef.current = { destroy };162163return () => {164if (cursorRef.current) {165cursorRef.current.destroy();166}167prefersReducedMotion.removeEventListener(168'change',169handleReducedMotionChange170);171};172}, [options]);173174return null;175};176177export default TextFlagCursor;
Prop | Type | Default | Description |
---|---|---|---|
classname | string | Optional CSS class for styling the main vignette container. | |
children | React.ReactNode | The content to display inside the vignette effect. | |
radius | string | 24px | The radius for the vignette effect. |
inset | string | 20px | The inset value for the vignette effect. |
transitionLength | string | 44px | The length of the transition effect applied to the vignette. |
blur | string | 6px | The blur amount for the vignette effect. |
blurclassname | string | Optional CSS class for styling the blur effect container. |