Glitch Text
RGB channel-split distortion with scanlines and noise on hover — cyberpunk aesthetic
textglitchdistortionhovercyberpunkanimation
Preview
Source Code
"use client";
import { useState, useRef, useEffect, useCallback } from "react";
interface GlitchTextProps {
text: string;
className?: string;
glitchOnHover?: boolean;
autoGlitch?: boolean;
autoGlitchInterval?: number;
intensity?: number;
}
export default function GlitchText({
text,
className = "",
glitchOnHover = true,
autoGlitch = false,
autoGlitchInterval = 3000,
intensity = 1,
}: GlitchTextProps) {
const [isGlitching, setIsGlitching] = useState(false);
const containerRef = useRef<HTMLSpanElement>(null);
const animFrameRef = useRef<number>(0);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const triggerGlitch = useCallback(() => {
setIsGlitching(true);
setTimeout(() => setIsGlitching(false), 200 + intensity * 100);
}, [intensity]);
// Auto-glitch mode
useEffect(() => {
if (!autoGlitch) return;
const interval = setInterval(triggerGlitch, autoGlitchInterval);
return () => clearInterval(interval);
}, [autoGlitch, autoGlitchInterval, triggerGlitch]);
// Canvas-based scanline + noise overlay
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let canvas = canvasRef.current;
if (!canvas) {
canvas = document.createElement("canvas");
canvas.style.cssText =
"position:absolute;inset:0;pointer-events:none;mix-blend-mode:screen;z-index:2;";
container.appendChild(canvas);
canvasRef.current = canvas;
}
const ctx = canvas.getContext("2d");
if (!ctx) return;
const animate = () => {
const w = container.offsetWidth;
const h = container.offsetHeight;
canvas!.width = w;
canvas!.height = h;
ctx.clearRect(0, 0, w, h);
if (isGlitching) {
// Random RGB shift blocks
const blockCount = Math.floor(3 + intensity * 4);
for (let i = 0; i < blockCount; i++) {
const bx = Math.random() * w;
const by = Math.random() * h;
const bw = 20 + Math.random() * w * 0.4;
const bh = 2 + Math.random() * 6;
ctx.fillStyle = `rgba(${Math.random() > 0.5 ? "255,0,0" : "0,255,255"},${0.08 + Math.random() * 0.12})`;
ctx.fillRect(bx + (Math.random() - 0.5) * 10 * intensity, by, bw, bh);
}
// Scanlines
ctx.fillStyle = "rgba(0,0,0,0.03)";
for (let y = 0; y < h; y += 2) {
ctx.fillRect(0, y, w, 1);
}
// Noise pixels
const noiseCount = Math.floor(15 * intensity);
for (let i = 0; i < noiseCount; i++) {
const nx = Math.random() * w;
const ny = Math.random() * h;
ctx.fillStyle = `rgba(255,255,255,${0.1 + Math.random() * 0.15})`;
ctx.fillRect(nx, ny, Math.random() * 3 + 1, 1);
}
}
animFrameRef.current = requestAnimationFrame(animate);
};
animate();
return () => cancelAnimationFrame(animFrameRef.current);
}, [isGlitching, intensity]);
// Clean up canvas on unmount
useEffect(() => {
return () => {
if (canvasRef.current && containerRef.current) {
containerRef.current.removeChild(canvasRef.current);
canvasRef.current = null;
}
};
}, []);
const glitchOffset = isGlitching ? intensity * 3 : 0;
return (
<span
ref={containerRef}
className={`relative inline-block ${className}`}
onMouseEnter={glitchOnHover ? triggerGlitch : undefined}
style={{ position: "relative" }}
>
{/* Red channel offset */}
<span
aria-hidden="true"
className="absolute inset-0 z-0"
style={{
color: "rgba(255, 0, 0, 0.7)",
transform: isGlitching
? `translate(${glitchOffset}px, ${-glitchOffset * 0.5}px)`
: "none",
clipPath: isGlitching
? `polygon(0 ${15 + Math.random() * 20}%, 100% ${15 + Math.random() * 20}%, 100% ${50 + Math.random() * 20}%, 0 ${50 + Math.random() * 20}%)`
: "none",
transition: "none",
}}
>
{text}
</span>
{/* Cyan channel offset */}
<span
aria-hidden="true"
className="absolute inset-0 z-0"
style={{
color: "rgba(0, 255, 255, 0.7)",
transform: isGlitching
? `translate(${-glitchOffset}px, ${glitchOffset * 0.5}px)`
: "none",
clipPath: isGlitching
? `polygon(0 ${40 + Math.random() * 20}%, 100% ${40 + Math.random() * 20}%, 100% ${70 + Math.random() * 20}%, 0 ${70 + Math.random() * 20}%)`
: "none",
transition: "none",
}}
>
{text}
</span>
{/* Main text */}
<span
className="relative z-10"
style={{
transform: isGlitching
? `translate(${(Math.random() - 0.5) * 2 * intensity}px, 0)`
: "none",
}}
>
{text}
</span>
</span>
);
}