Fuzzy Text

CRT television static noise effect with chromatic aberration, scanlines, and jitter. Text appears fuzzy with VHS/old TV static look, clearing on hover.

Classic Fuzzy Static

Text with continuous noise/static effect that clears on hover.

Hover to Clear

Green terminal style - hover to clear the static and reveal clean text.

404 Error Style

High intensity noise that never clears - perfect for error pages.

Installation

npm install framer-motion
pnpm install framer-motion
yarn add framer-motion
bun add framer-motion

Copy and paste the following code into your project.

'use client';import { useEffect, useRef, useState, useCallback, useMemo } from 'react';import { motion, useInView } from 'framer-motion';interface FuzzyTextProps {  text: string;  className?: string;  fontSize?: string;  noiseIntensity?: number;  enableHover?: boolean;  baseColor?: string;  glowColor?: string;  speed?: number;}export const FuzzyText = ({  text,  className = '',  fontSize = '4rem',  noiseIntensity = 1,  enableHover = true,  baseColor = '#fff',  glowColor = '#fff',  speed = 50,}: Readonly<FuzzyTextProps>) => {  const containerRef = useRef<HTMLSpanElement>(null);  const [isHovered, setIsHovered] = useState(false);  const [displayText, setDisplayText] = useState(text);  const animationRef = useRef<NodeJS.Timeout | null>(null);  // Noise characters for the fuzzy effect  const noiseChars = useMemo(    () => '!<>-_\\/[]{}—=+*^?#_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',    [],  );  const getRandomChar = useCallback(() => {    return noiseChars[Math.floor(Math.random() * noiseChars.length)];  }, [noiseChars]);  // Apply noise to text when not hovered  useEffect(() => {    if (!enableHover) {      // Always show noise when hover is disabled      animationRef.current = setInterval(() => {        const noisyText = text          .split('')          .map((char) => {            if (char === ' ') return char;            return Math.random() < noiseIntensity * 0.3 ? getRandomChar() : char;          })          .join('');        setDisplayText(noisyText);      }, speed);      return () => {        if (animationRef.current) clearInterval(animationRef.current);      };    }    if (!isHovered) {      // Apply noise effect when not hovered      animationRef.current = setInterval(() => {        const noisyText = text          .split('')          .map((char) => {            if (char === ' ') return char;            return Math.random() < noiseIntensity * 0.3 ? getRandomChar() : char;          })          .join('');        setDisplayText(noisyText);      }, speed);    } else {      // Show clear text when hovered      setDisplayText(text);      if (animationRef.current) {        clearInterval(animationRef.current);      }    }    return () => {      if (animationRef.current) clearInterval(animationRef.current);    };  }, [isHovered, text, noiseIntensity, enableHover, getRandomChar, speed]);  const uniqueId = useMemo(    () => `fuzzy-${Math.random().toString(36).substr(2, 9)}`,    [],  );  return (    <span      ref={containerRef}      className={`relative inline-block cursor-pointer ${className}`}      onMouseEnter={() => setIsHovered(true)}      onMouseLeave={() => setIsHovered(false)}      style={{        fontSize,        fontWeight: 'bold',      }}    >      {/* SVG Filter for noise effect */}      <svg width="0" height="0" className="absolute">        <defs>          <filter id={`${uniqueId}-noise`}>            <feTurbulence              type="fractalNoise"              baseFrequency={isHovered ? '0' : '0.8'}              numOctaves="4"              seed={Math.random() * 100}              stitchTiles="stitch"              result="noise"            />            <feDisplacementMap              in="SourceGraphic"              in2="noise"              scale={isHovered ? 0 : 2 * noiseIntensity}              xChannelSelector="R"              yChannelSelector="G"            />          </filter>        </defs>      </svg>      {/* Main text with noise effect */}      <motion.span        className="relative inline-block"        style={{          color: baseColor,          filter: `url(#${uniqueId}-noise)`,          textShadow: `            0 0 ${isHovered ? 20 : 5}px ${glowColor},            0 0 ${isHovered ? 40 : 10}px ${glowColor},            0 0 ${isHovered ? 60 : 15}px ${glowColor}          `,        }}        animate={{          x: isHovered ? 0 : [0, -1, 1, -1, 0],          y: isHovered ? 0 : [0, 1, -1, 1, 0],        }}        transition={{          duration: 0.1,          repeat: isHovered ? 0 : Infinity,          repeatType: 'loop',        }}      >        {displayText}      </motion.span>      {/* Chromatic aberration layers */}      {!isHovered && (        <>          {/* Red channel offset */}          <motion.span            className="pointer-events-none absolute left-0 top-0"            style={{              color: 'transparent',              textShadow: `0 0 2px rgba(255, 0, 0, ${0.4 * noiseIntensity})`,              mixBlendMode: 'screen',            }}            animate={{              x: [-2, 2, -2],              opacity: [0.5, 0.8, 0.5],            }}            transition={{              duration: 0.15,              repeat: Infinity,              repeatType: 'reverse',            }}          >            {displayText}          </motion.span>          {/* Blue channel offset */}          <motion.span            className="pointer-events-none absolute left-0 top-0"            style={{              color: 'transparent',              textShadow: `0 0 2px rgba(0, 100, 255, ${0.4 * noiseIntensity})`,              mixBlendMode: 'screen',            }}            animate={{              x: [2, -2, 2],              opacity: [0.5, 0.8, 0.5],            }}            transition={{              duration: 0.15,              repeat: Infinity,              repeatType: 'reverse',            }}          >            {displayText}          </motion.span>        </>      )}      {/* Scanlines */}      <motion.span        className="pointer-events-none absolute inset-0 z-10"        style={{          background: `repeating-linear-gradient(            0deg,            transparent,            transparent 2px,            rgba(0, 0, 0, ${isHovered ? 0.05 : 0.15}) 2px,            rgba(0, 0, 0, ${isHovered ? 0.05 : 0.15}) 4px          )`,        }}        animate={{          backgroundPosition: ['0 0', '0 4px'],        }}        transition={{          duration: 0.5,          repeat: Infinity,          ease: 'linear',        }}      />      {/* Occasional glitch flicker */}      <motion.span        className="pointer-events-none absolute inset-0"        style={{          background: 'rgba(255, 255, 255, 0.03)',        }}        animate={{          opacity: isHovered ? 0 : [0, 1, 0, 0, 1, 0],        }}        transition={{          duration: 0.5,          repeat: Infinity,          times: [0, 0.1, 0.12, 0.8, 0.82, 1],        }}      />    </span>  );};export default FuzzyText;

Props

Prop

Type

Examples

// White text on black background
<FuzzyText
  text="STATIC NOISE"
  baseColor="#ffffff"
  glowColor="#ffffff"
  enableHover={true}
/>

// Green terminal style
<FuzzyText
  text="ACCESS DENIED"
  baseColor="#00ff00"
  glowColor="#00ff00"
  noiseIntensity={1.2}
/>

// Red error/warning
<FuzzyText
  text="404 NOT FOUND"
  baseColor="#ff0040"
  glowColor="#ff0040"
  enableHover={false}
/>

// Subtle effect
<FuzzyText
  text="Subtle Glitch"
  noiseIntensity={0.5}
  speed={80}
/>

// Fast intense noise
<FuzzyText
  text="CHAOS"
  noiseIntensity={2}
  speed={20}
/>