effects#glitch#cyberpunk
Glitch Distortion
Aggressive chromatic aberration with RGB channel splitting, scanlines, and random displacement bursts
Implementation Guide
# Glitch Distortion Effect
> Aggressive chromatic aberration with RGB channel splitting, scanlines, and random displacement bursts. A cyberpunk/corrupted data aesthetic perfect for edgy landing pages, game sites, or tech brands.
## Quick Start
```tsx
import GlitchDistortion from '@/components/examples/effects/glitch-distortion';
export default function Hero() {
return (
<div className="relative h-screen">
<GlitchDistortion />
<div className="relative z-10">{/* Your content goes here */}</div>
</div>
);
}
```
## Props
| Prop | Type | Default | Description |
| --------------- | ----------------------------------------------- | ----------------------------------------------------- | -------------------------------------------- |
| colors | { red?: string; green?: string; blue?: string } | { red: '#FF3B30', green: '#34C759', blue: '#007AFF' } | RGB channel colors for chromatic split |
| glitchIntensity | number | 1 | Overall glitch intensity multiplier (0-3) |
| scanlineOpacity | number | 0.08 | CRT scanline overlay opacity (0-1) |
| burstFrequency | number | 0.03 | Probability of glitch bursts per frame (0-1) |
## Full Implementation
```tsx
'use client';
import { useEffect, useRef, useCallback } from 'react';
interface GlitchDistortionProps {
colors?: {
red?: string;
green?: string;
blue?: string;
};
glitchIntensity?: number;
scanlineOpacity?: number;
burstFrequency?: number;
}
interface GlitchSlice {
y: number;
height: number;
offsetX: number;
rgbOffset: number;
duration: number;
startTime: number;
}
export default function GlitchDistortion({
colors = {
red: '#FF3B30',
green: '#34C759',
blue: '#007AFF',
},
glitchIntensity = 1,
scanlineOpacity = 0.08,
burstFrequency = 0.03,
}: GlitchDistortionProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>(0);
const mouseRef = useRef({ x: 0.5, y: 0.5 });
const glitchSlicesRef = useRef<GlitchSlice[]>([]);
const timeRef = useRef(0);
const lastBurstRef = useRef(0);
const hexToRgb = useCallback((hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: { r: 255, g: 255, b: 255 };
}, []);
const createGlitchBurst = useCallback(
(canvasHeight: number, currentTime: number) => {
const sliceCount = Math.floor(Math.random() * 8) + 3;
const newSlices: GlitchSlice[] = [];
for (let i = 0; i < sliceCount; i++) {
newSlices.push({
y: Math.random() * canvasHeight,
height: Math.random() * 30 + 5,
offsetX: (Math.random() - 0.5) * 50 * glitchIntensity,
rgbOffset: (Math.random() - 0.5) * 15 * glitchIntensity,
duration: Math.random() * 150 + 50,
startTime: currentTime + Math.random() * 100,
});
}
return newSlices;
},
[glitchIntensity]
);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
let prefersReducedMotion = false;
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = motionQuery.matches;
const redColor = hexToRgb(colors.red || '#FF3B30');
const greenColor = hexToRgb(colors.green || '#34C759');
const blueColor = hexToRgb(colors.blue || '#007AFF');
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
};
const drawScanlines = (width: number, height: number, time: number) => {
ctx.save();
// CRT scanlines
const scanlineHeight = 2;
const flickerIntensity = prefersReducedMotion ? 0 : Math.sin(time * 0.01) * 0.02;
ctx.fillStyle = `rgba(0, 0, 0, ${scanlineOpacity + flickerIntensity})`;
for (let y = 0; y < height; y += scanlineHeight * 2) {
ctx.fillRect(0, y, width, scanlineHeight);
}
ctx.restore();
};
const drawChromaticAberration = (
width: number,
height: number,
mouseX: number,
mouseY: number,
time: number
) => {
const baseOffset = 3 * glitchIntensity;
const mouseInfluence = prefersReducedMotion ? 0 : 1;
// Calculate offset based on mouse position
const distanceFromCenter = Math.sqrt(Math.pow(mouseX - 0.5, 2) + Math.pow(mouseY - 0.5, 2));
const dynamicOffset = baseOffset + distanceFromCenter * 10 * mouseInfluence * glitchIntensity;
// Time-based jitter
const jitterX = prefersReducedMotion ? 0 : Math.sin(time * 0.05) * 2 * glitchIntensity;
const jitterY = prefersReducedMotion ? 0 : Math.cos(time * 0.07) * 2 * glitchIntensity;
ctx.save();
ctx.globalCompositeOperation = 'screen';
// Red channel (offset left-up)
ctx.fillStyle = `rgba(${redColor.r}, ${redColor.g}, ${redColor.b}, 0.15)`;
ctx.fillRect(-dynamicOffset + jitterX, -dynamicOffset + jitterY, width, height);
// Green channel (center)
ctx.fillStyle = `rgba(${greenColor.r}, ${greenColor.g}, ${greenColor.b}, 0.1)`;
ctx.fillRect(jitterX * 0.5, jitterY * 0.5, width, height);
// Blue channel (offset right-down)
ctx.fillStyle = `rgba(${blueColor.r}, ${blueColor.g}, ${blueColor.b}, 0.15)`;
ctx.fillRect(dynamicOffset + jitterX, dynamicOffset + jitterY, width, height);
ctx.restore();
};
const drawGlitchSlices = (width: number, height: number, currentTime: number) => {
// Clean up expired slices
glitchSlicesRef.current = glitchSlicesRef.current.filter(
(slice) => currentTime < slice.startTime + slice.duration
);
if (prefersReducedMotion) return;
ctx.save();
for (const slice of glitchSlicesRef.current) {
if (currentTime < slice.startTime) continue;
const progress = (currentTime - slice.startTime) / slice.duration;
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentOffset = slice.offsetX * (1 - easeOut);
const currentRgbOffset = slice.rgbOffset * (1 - easeOut);
// Draw displaced slice with RGB split
ctx.globalCompositeOperation = 'screen';
// Red slice
ctx.fillStyle = `rgba(${redColor.r}, 0, 0, 0.3)`;
ctx.fillRect(currentOffset - currentRgbOffset, slice.y, width, slice.height);
// Cyan slice (opposite)
ctx.fillStyle = `rgba(0, ${greenColor.g}, ${blueColor.b}, 0.2)`;
ctx.fillRect(-currentOffset + currentRgbOffset, slice.y, width, slice.height);
}
ctx.restore();
};
const drawNoiseLines = (width: number, height: number, time: number) => {
if (prefersReducedMotion) return;
ctx.save();
ctx.globalAlpha = 0.05;
// Random horizontal noise lines
const lineCount = Math.floor(Math.random() * 3);
for (let i = 0; i < lineCount; i++) {
const y = Math.random() * height;
const lineWidth = Math.random() * width * 0.3;
const x = Math.random() * (width - lineWidth);
ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.5})`;
ctx.fillRect(x, y, lineWidth, 1);
}
// Occasional full-width flash line
if (Math.random() < 0.02 * glitchIntensity) {
const y = Math.random() * height;
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
ctx.fillRect(0, y, width, 2);
}
ctx.restore();
};
const render = (time: number) => {
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
// Dark background
const gradient = ctx.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, '#0a0a0a');
gradient.addColorStop(1, '#0f0f1a');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// Chromatic aberration layer
drawChromaticAberration(width, height, mouseRef.current.x, mouseRef.current.y, time);
// Glitch slices
drawGlitchSlices(width, height, time);
// Noise lines
drawNoiseLines(width, height, time);
// Scanlines overlay
drawScanlines(width, height, time);
// Random glitch bursts
if (!prefersReducedMotion && Math.random() < burstFrequency * glitchIntensity) {
if (time - lastBurstRef.current > 200) {
const newSlices = createGlitchBurst(height, time);
glitchSlicesRef.current = [...glitchSlicesRef.current, ...newSlices];
lastBurstRef.current = time;
}
}
};
const animate = (timestamp: number) => {
timeRef.current = timestamp;
render(timestamp);
if (!prefersReducedMotion) {
animationRef.current = requestAnimationFrame(animate);
}
};
const handleMouseMove = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouseRef.current = {
x: (event.clientX - rect.left) / rect.width,
y: (event.clientY - rect.top) / rect.height,
};
};
const handleMouseLeave = () => {
mouseRef.current = { x: 0.5, y: 0.5 };
};
const handleMotionChange = (event: MediaQueryListEvent) => {
prefersReducedMotion = event.matches;
if (prefersReducedMotion) {
cancelAnimationFrame(animationRef.current);
glitchSlicesRef.current = [];
render(timeRef.current);
} else {
animationRef.current = requestAnimationFrame(animate);
}
};
motionQuery.addEventListener('change', handleMotionChange);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseleave', handleMouseLeave);
resizeCanvas();
if (prefersReducedMotion) {
// Render static version
render(0);
} else {
animationRef.current = requestAnimationFrame(animate);
}
const handleResize = () => {
resizeCanvas();
if (prefersReducedMotion) {
render(timeRef.current);
}
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationRef.current);
window.removeEventListener('resize', handleResize);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('mouseleave', handleMouseLeave);
motionQuery.removeEventListener('change', handleMotionChange);
};
}, [colors, glitchIntensity, scanlineOpacity, burstFrequency, hexToRgb, createGlitchBurst]);
return (
<canvas
ref={canvasRef}
className="h-full w-full"
style={{
display: 'block',
cursor: 'crosshair',
}}
/>
);
}
```
## Customization
### Cyberpunk Neon
Bright neon colors with heavy glitching:
```tsx
<GlitchDistortion
colors={{
red: '#FF0055',
green: '#00FF88',
blue: '#00BBFF',
}}
glitchIntensity={1.5}
burstFrequency={0.05}
/>
```
### Subtle Data Corruption
Minimal effect for professional sites:
```tsx
<GlitchDistortion
colors={{
red: '#FF6B6B',
green: '#4ECDC4',
blue: '#45B7D1',
}}
glitchIntensity={0.5}
scanlineOpacity={0.04}
burstFrequency={0.01}
/>
```
### Retro CRT Monitor
Heavy scanlines with classic RGB:
```tsx
<GlitchDistortion
colors={{
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF',
}}
scanlineOpacity={0.15}
glitchIntensity={0.8}
/>
```
### Aggressive Chaos
Maximum glitch effect:
```tsx
<GlitchDistortion glitchIntensity={3} burstFrequency={0.1} scanlineOpacity={0.1} />
```
### Monochrome Glitch
Single color channel emphasis:
```tsx
<GlitchDistortion
colors={{
red: '#FF3366',
green: '#FF3366',
blue: '#330011',
}}
glitchIntensity={1.2}
/>
```
### Brand Colors Example
Using your brand palette:
```tsx
<GlitchDistortion
colors={{
red: '#FF3B30', // Your accent
green: '#5856D6', // Your secondary
blue: '#007AFF', // Your primary
}}
glitchIntensity={0.8}
/>
```
## Performance Considerations
- **Canvas-based rendering** - Uses hardware-accelerated 2D canvas for smooth 60fps
- **Efficient slice cleanup** - Expired glitch slices are automatically removed from memory
- **Minimal allocations** - Reuses refs to avoid garbage collection pauses
- **Mouse throttling** - Mouse position updates on native events without additional throttling needed
- **Burst cooldown** - Prevents burst spam with 200ms minimum interval between bursts
- **Reduced motion support** - Falls back to static rendering with no animation overhead
### Mobile Optimization
For mobile devices, reduce intensity for better battery life:
```tsx
<GlitchDistortion glitchIntensity={0.5} burstFrequency={0.02} scanlineOpacity={0.05} />
```
### Heavy Usage Warning
High glitch intensity with frequent bursts can be visually intense. Consider user comfort:
```tsx
// Accessible default
<GlitchDistortion glitchIntensity={1} burstFrequency={0.03} />
// NOT recommended for extended viewing
<GlitchDistortion glitchIntensity={3} burstFrequency={0.15} />
```
## Accessibility
- **Respects `prefers-reduced-motion`** - All animations stop completely, showing only static chromatic aberration and scanlines
- **Listens for preference changes** - Dynamically responds if user toggles motion preferences during session
- **No flashing content** - Glitch bursts are color shifts, not bright flashes, to avoid seizure triggers
- **Crosshair cursor** - Visual indicator that the element is interactive
- **Purely decorative** - Effect is background enhancement; ensure content above has proper contrast
### Reduced Motion Behavior
When `prefers-reduced-motion` is enabled:
- No glitch bursts or slice animations
- No time-based jitter on RGB channels
- Scanlines display without flicker
- Static chromatic aberration based on center position
### Photosensitivity Considerations
For users who may be sensitive to visual effects:
```tsx
// Gentler version suitable for broader audiences
<GlitchDistortion glitchIntensity={0.3} burstFrequency={0.005} scanlineOpacity={0.03} />
```
## Browser Support
- **Canvas 2D API** - All modern browsers (Chrome, Firefox, Safari, Edge)
- **requestAnimationFrame** - IE10+ and all modern browsers
- **globalCompositeOperation 'screen'** - Full support in all modern browsers
- **matchMedia** - IE10+ for `prefers-reduced-motion` detection
- **devicePixelRatio** - Full support for retina/HiDPI displays
### Fallback Considerations
For browsers without Canvas support, add a CSS gradient fallback:
```tsx
<div className="relative h-full w-full">
{/* CSS fallback */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(135deg, #0a0a0a 0%, #0f0f1a 100%)',
}}
/>
{/* Canvas glitch overlay */}
<GlitchDistortion />
</div>
```