CRT-style visual effects with scanlines, chromatic aberration, and glitch tears — add to your site in seconds
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Wrap any content with distortion effects. Supports barrel distortion, chromatic aberration, CRT scanlines, grid overlay, vignette, film grain, and random glitch tears.
/*
================================================================================
AI COMPONENT: GlitchDistortion
================================================================================
SETUP:
1. Create file: components/GlitchDistortion.tsx
2. Copy this entire code block into that file
3. Import: import GlitchDistortion from "@/components/GlitchDistortion"
4. Uses VFX-JS library (loaded from CDN automatically)
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default | Description |
|-------------|---------|---------|------------------------------------------|
| distortion | boolean | true | Enable barrel/fisheye distortion |
| aberration | number | 0.5 | Chromatic aberration strength (0-1) |
| scanlines | boolean | true | Enable CRT scanlines overlay |
| grid | boolean | true | Enable grid pattern overlay |
| vignette | number | 0.8 | Vignette/edge darkening strength (0-1) |
| grain | number | 0.1 | Film grain/noise amount (0-1) |
| glitchTears | boolean | true | Enable random horizontal glitch tears |
================================================================================
COMMON MODIFICATIONS
================================================================================
1. FULL EFFECT (default):
<GlitchDistortion>
<h1>Your content</h1>
<img src="..." />
</GlitchDistortion>
2. SUBTLE EFFECT (less intense):
<GlitchDistortion aberration={0.2} grain={0.05} glitchTears={false}>
...
</GlitchDistortion>
3. CRT MONITOR STYLE:
<GlitchDistortion scanlines={true} grid={false} vignette={1}>
...
</GlitchDistortion>
4. GLITCH-ONLY (no distortion):
<GlitchDistortion distortion={false} scanlines={false} glitchTears={true}>
...
</GlitchDistortion>
5. CINEMATIC (vignette + grain only):
<GlitchDistortion distortion={false} scanlines={false} grid={false}
glitchTears={false} vignette={0.9} grain={0.15}>
...
</GlitchDistortion>
================================================================================
IMPORTANT NOTES
================================================================================
- Wrap your content (text, images) with the component
- Supported elements: img, h1, h2, h3, p, span.vfx
- Add data-z attribute for z-index control: <h2 data-z="1">...</h2>
- Works best on dark backgrounds
- WebGL required (modern browsers)
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useEffect, useRef, useState } from "react";
export interface GlitchDistortionProps {
children: React.ReactNode;
className?: string;
/** Enable barrel/fisheye distortion */
distortion?: boolean;
/** Chromatic aberration strength (0-1) */
aberration?: number;
/** Enable scanlines overlay */
scanlines?: boolean;
/** Enable grid overlay */
grid?: boolean;
/** Vignette strength (0-1) */
vignette?: number;
/** Film grain/noise amount (0-1) */
grain?: number;
/** Enable random glitch tears on elements */
glitchTears?: boolean;
}
const defaultProps: Required<Omit<GlitchDistortionProps, "children" | "className">> = {
distortion: true,
aberration: 0.5,
scanlines: true,
grid: true,
vignette: 0.8,
grain: 0.1,
glitchTears: true,
};
export default function GlitchDistortion({
children,
className = "",
distortion = defaultProps.distortion,
aberration = defaultProps.aberration,
scanlines = defaultProps.scanlines,
grid = defaultProps.grid,
vignette = defaultProps.vignette,
grain = defaultProps.grain,
glitchTears = defaultProps.glitchTears,
}: GlitchDistortionProps) {
const containerRef = useRef<HTMLDivElement>(null);
const vfxRef = useRef<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
let mounted = true;
async function initVFX() {
try {
const { VFX } = await import("https://esm.sh/@vfx-js/core@0.8.0" as any);
if (!mounted || !containerRef.current) return;
const postShader = createPostShader({
distortion,
aberration,
scanlines,
grid,
vignette,
grain,
});
const elementShader = createElementShader({ glitchTears });
vfxRef.current = new VFX({
scrollPadding: false,
postEffect: { shader: postShader },
});
let elementIndex = 0;
const elements = containerRef.current.querySelectorAll("img,h1,h2,h3,p,span.vfx");
for (const el of elements) {
const zAttr = el.getAttribute("data-z");
vfxRef.current.add(el, {
shader: elementShader,
uniforms: { id: elementIndex++ },
zIndex: zAttr ? parseInt(zAttr) : 0,
});
}
setIsLoaded(true);
} catch (err) {
console.error("Failed to load VFX-JS:", err);
}
}
initVFX();
return () => {
mounted = false;
if (vfxRef.current) {
vfxRef.current.dispose?.();
vfxRef.current = null;
}
};
}, [distortion, aberration, scanlines, grid, vignette, grain, glitchTears]);
return (
<div
ref={containerRef}
className={className}
style={{ opacity: isLoaded ? 1 : 0, transition: "opacity 0.3s" }}
>
{children}
</div>
);
}
function createPostShader(options: {
distortion: boolean;
aberration: number;
scanlines: boolean;
grid: boolean;
vignette: number;
grain: number;
}) {
const { distortion, aberration, scanlines, grid, vignette, grain } = options;
return `
precision highp float;
uniform sampler2D src;
uniform vec2 offset;
uniform vec2 resolution;
uniform float time;
out vec4 outColor;
vec4 readTex(vec2 uv) {
vec4 c = texture(src, uv);
c.a *= smoothstep(.5, .499, abs(uv.x - .5)) * smoothstep(.5, .499, abs(uv.y - .5));
return c;
}
vec2 zoom(vec2 uv, float t) {
return (uv - .5) * t + .5;
}
float rand(vec3 p) {
return fract(sin(dot(p, vec3(829., 4839., 432.))) * 39428.);
}
void main() {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
vec2 p = uv * 2. - 1.;
p.x *= resolution.x / resolution.y;
float l = length(p);
// distortion
${distortion ? `
float dist = pow(l, 2.) * .3;
dist = smoothstep(0., 1., dist);
uv = zoom(uv, 0.5 + dist);
` : ""}
// blur
vec2 du = (uv - .5);
float a = atan(p.y, p.x);
float rd = rand(vec3(a, time, 0));
uv = (uv - .5) * (1.0 + rd * pow(l * 0.7, 3.) * 0.3) + .5;
vec2 uvr = uv;
vec2 uvg = uv;
vec2 uvb = uv;
// aberration
${aberration > 0 ? `
float aberrationStrength = ${aberration.toFixed(3)};
float d = (1. + sin(uv.y * 20. + time * 3.) * 0.1) * 0.05 * aberrationStrength;
uvr.x += 0.0015 * aberrationStrength;
uvb.x -= 0.0015 * aberrationStrength;
uvr = zoom(uvr, 1. + d * l * l);
uvb = zoom(uvb, 1. - d * l * l);
` : ""}
vec4 cr = readTex(uvr);
vec4 cg = readTex(uvg);
vec4 cb = readTex(uvb);
outColor = vec4(cr.r, cg.g, cb.b, (cr.a + cg.a + cb.a) / 1.);
vec4 deco = vec4(0.);
// scanline
${scanlines ? `
float res = resolution.y;
deco += (
sin(uv.y * res * .7 + time * 100.) *
sin(uv.y * res * .3 - time * 130.)
) * 0.05;
` : ""}
// grid
${grid ? `
deco += smoothstep(.01, .0, min(fract(uv.x * 20.), fract(uv.y * 20.))) * 0.1;
` : ""}
outColor += deco * smoothstep(2., 0., l);
// vignette
${vignette > 0 ? `
float vignetteStrength = ${vignette.toFixed(3)};
outColor *= (1.8 - l * l * vignetteStrength);
` : ""}
// dither/grain
${grain > 0 ? `
float grainAmount = ${grain.toFixed(3)};
outColor += rand(vec3(p, time)) * grainAmount;
` : ""}
}
`;
}
function createElementShader(options: { glitchTears: boolean }) {
const { glitchTears } = options;
return `
precision highp float;
uniform sampler2D src;
uniform vec2 offset;
uniform vec2 resolution;
uniform float time;
uniform float id;
out vec4 outColor;
vec4 readTex(vec2 uv) {
vec4 c = texture(src, uv);
c.a *= smoothstep(.5, .499, abs(uv.x - .5)) * smoothstep(.5, .499, abs(uv.y - .5));
return c;
}
float rand(vec2 p) {
return fract(sin(dot(p, vec2(829., 483.))) * 394.);
}
void main() {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
vec2 uvr = uv, uvg = uv, uvb = uv;
${glitchTears ? `
float r = rand(vec2(floor(time * 43.), id));
if (r > 0.8) {
float y = sin(floor(uv.y / 0.07)) + sin(floor(uv.y / 0.003 + time));
float f = rand(vec2(y, floor(time * 5.0) + id)) * 2. - 1.;
uvr.x += f * 0.1;
uvg.x += f * 0.2;
uvb.x += f * 0.3;
}
float r2 = rand(vec2(floor(time * 37.), id + 10.));
if (r2 > 0.9) {
uvr.x += sin(uv.y * 7. + time + id + 1.) * 0.03;
uvg.x += sin(uv.y * 5. + time + id + 2.) * 0.03;
uvb.x += sin(uv.y * 3. + time + id + 3.) * 0.03;
}
` : ""}
vec4 cr = readTex(uvr);
vec4 cg = readTex(uvg);
vec4 cb = readTex(uvb);
outColor = vec4(cr.r, cg.g, cb.b, (cr.a + cg.a + cb.a) / 1.);
}
`;
}
export { GlitchDistortion };import GlitchDistortion from "@/components/GlitchDistortion";
// Basic usage - wrap any content with distortion effects
export default function GlitchDemo() {
return (
<div className="bg-black min-h-screen">
<GlitchDistortion>
<h1>Distorted Title</h1>
<img src="/your-image.jpg" alt="Demo" />
<p>This text will have glitch effects applied.</p>
</GlitchDistortion>
</div>
);
}
// Subtle effect - less intense
export function SubtleGlitch() {
return (
<GlitchDistortion
aberration={0.2}
grain={0.05}
glitchTears={false}
>
<h1>Subtle Effect</h1>
<p>Less intense distortion for cleaner look.</p>
</GlitchDistortion>
);
}
// CRT monitor style
export function CRTStyle() {
return (
<GlitchDistortion
scanlines={true}
grid={false}
vignette={1}
glitchTears={false}
>
<h1>Retro CRT</h1>
<img src="/retro-image.jpg" alt="CRT" />
</GlitchDistortion>
);
}
// Cinematic (vignette + grain only)
export function CinematicStyle() {
return (
<GlitchDistortion
distortion={false}
scanlines={false}
grid={false}
glitchTears={false}
vignette={0.9}
grain={0.15}
>
<img src="/cinematic.jpg" alt="Film" />
</GlitchDistortion>
);
}