Implementing Glitch Effect on a Website
Glitch—a digital artifact as a design technique: horizontal shifts of image parts, chromatic aberration, flickering, digital "broken screen". Applied on hover, during page transitions, as heading animation.
CSS Glitch for text
Pure CSS via ::before/::after pseudo-elements with clip-path:
<h1 class="glitch" data-text="SYSTEM">SYSTEM</h1>
.glitch {
position: relative;
color: #fff;
font-size: 80px;
font-weight: 900;
letter-spacing: 0.05em;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.glitch::before {
left: 2px;
text-shadow: -2px 0 #ff0040;
animation: glitch-1 3s infinite linear alternate-reverse;
}
.glitch::after {
left: -2px;
text-shadow: 2px 0 #00ffea;
animation: glitch-2 2s infinite linear alternate-reverse;
}
@keyframes glitch-1 {
0% { clip-path: inset(20% 0 60% 0); }
5% { clip-path: inset(70% 0 1% 0); }
10% { clip-path: inset(40% 0 43% 0); }
15% { clip-path: inset(5% 0 80% 0); }
20% { clip-path: inset(60% 0 25% 0); }
/* ... most steps — empty for irregularity */
25% { clip-path: inset(0 0 100% 0); }
30% { clip-path: inset(0 0 100% 0); }
35% { clip-path: inset(0 0 100% 0); }
40% { clip-path: inset(15% 0 55% 0); }
45% { clip-path: inset(0 0 100% 0); }
50% { clip-path: inset(0 0 100% 0); }
55% { clip-path: inset(85% 0 5% 0); }
100% { clip-path: inset(0 0 100% 0); }
}
@keyframes glitch-2 {
0% { clip-path: inset(55% 0 30% 0); transform: translate(-2px); }
10% { clip-path: inset(10% 0 75% 0); transform: translate(2px); }
20% { clip-path: inset(0 0 100% 0); }
30% { clip-path: inset(90% 0 3% 0); transform: translate(-1px); }
50% { clip-path: inset(0 0 100% 0); }
60% { clip-path: inset(45% 0 40% 0); transform: translate(3px); }
100% { clip-path: inset(0 0 100% 0); }
}
Glitch for images—chromatic aberration
RGB channel separation—the most recognizable glitch effect:
.glitch-image {
position: relative;
overflow: hidden;
}
.glitch-image img,
.glitch-image::before,
.glitch-image::after {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Red channel — shifted left */
.glitch-image::before {
content: '';
background: inherit;
background-image: var(--img);
mix-blend-mode: screen;
filter: url('#red-channel'); /* SVG filter: red only */
animation: glitch-ca-r 4s steps(1) infinite;
}
/* Blue channel — shifted right */
.glitch-image::after {
content: '';
background-image: var(--img);
mix-blend-mode: screen;
filter: url('#blue-channel');
animation: glitch-ca-b 4s steps(1) infinite;
}
@keyframes glitch-ca-r {
0%, 90%, 100% { transform: translate(0); clip-path: inset(0 0 100% 0); }
92% { transform: translate(-4px, 1px); clip-path: inset(30% 0 50% 0); }
94% { transform: translate(-6px, 0); clip-path: inset(10% 0 70% 0); }
96% { transform: translate(-3px, -1px); clip-path: inset(60% 0 20% 0); }
}
@keyframes glitch-ca-b {
0%, 90%, 100% { transform: translate(0); clip-path: inset(0 0 100% 0); }
92% { transform: translate(4px, -1px); clip-path: inset(30% 0 50% 0); }
94% { transform: translate(6px, 0); clip-path: inset(10% 0 70% 0); }
96% { transform: translate(3px, 1px); clip-path: inset(60% 0 20% 0); }
}
SVG filters for channel isolation:
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="red-channel">
<feColorMatrix type="matrix"
values="1 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0"/>
</filter>
<filter id="blue-channel">
<feColorMatrix type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 1 0"/>
</filter>
</defs>
</svg>
JavaScript Glitch—controlled trigger
class GlitchEffect {
private el: HTMLElement
private isGlitching = false
private timeouts: ReturnType<typeof setTimeout>[] = []
constructor(el: HTMLElement) {
this.el = el
}
trigger(duration = 600) {
if (this.isGlitching) return
this.isGlitching = true
this.el.classList.add('is-glitching')
// Several random flashes during duration
const flashes = Math.floor(Math.random() * 4) + 2
for (let i = 0; i < flashes; i++) {
const delay = Math.random() * duration * 0.7
const flashDuration = Math.random() * 80 + 40
this.timeouts.push(
setTimeout(() => {
this.el.classList.add('glitch-flash')
this.el.style.setProperty('--glitch-x', `${(Math.random() - 0.5) * 10}px`)
this.el.style.setProperty('--glitch-y', `${(Math.random() - 0.5) * 5}px`)
setTimeout(() => {
this.el.classList.remove('glitch-flash')
}, flashDuration)
}, delay)
)
}
this.timeouts.push(
setTimeout(() => {
this.el.classList.remove('is-glitching')
this.isGlitching = false
this.timeouts = []
}, duration)
)
}
destroy() {
this.timeouts.forEach(clearTimeout)
this.el.classList.remove('is-glitching', 'glitch-flash')
}
}
// Hover trigger
document.querySelectorAll<HTMLElement>('[data-glitch]').forEach((el) => {
const effect = new GlitchEffect(el)
el.addEventListener('mouseenter', () => effect.trigger(800))
})
Canvas Glitch—row-by-row shift
Maximum authenticity: random pixel rows shift horizontally, imitating video signal failure.
class CanvasGlitch {
private canvas: HTMLCanvasElement
private ctx: CanvasRenderingContext2D
private source: HTMLImageElement | HTMLVideoElement
private rafId: number | null = null
constructor(source: HTMLImageElement | HTMLVideoElement) {
this.source = source
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')!
// Replace source with canvas
source.parentNode?.insertBefore(this.canvas, source)
source.style.display = 'none'
this.canvas.width = source instanceof HTMLImageElement
? source.naturalWidth
: source.videoWidth
this.canvas.height = source instanceof HTMLImageElement
? source.naturalHeight
: source.videoHeight
}
renderFrame() {
const { width, height } = this.canvas
this.ctx.drawImage(this.source, 0, 0)
// Random rows shift
const numSlices = Math.floor(Math.random() * 8) + 2
for (let i = 0; i < numSlices; i++) {
const y = Math.floor(Math.random() * height)
const sliceHeight = Math.floor(Math.random() * 30) + 5
const offset = (Math.random() - 0.5) * 40
const imageData = this.ctx.getImageData(0, y, width, sliceHeight)
this.ctx.putImageData(imageData, offset, y)
}
// Chromatic aberration — red channel shift
const imageData = this.ctx.getImageData(0, 0, width, height)
const data = imageData.data
for (let i = 0; i < data.length; i += 4) {
const x = (i / 4) % width
const y = Math.floor(i / 4 / width)
const shift = 3 // pixels
if (x + shift < width) {
const j = (y * width + x + shift) * 4
data[i] = data[j] // R channel from right
}
}
this.ctx.putImageData(imageData, 0, 0)
}
startContinuous(fps = 8) {
let lastTime = 0
const interval = 1000 / fps
const tick = (time: number) => {
if (time - lastTime > interval) {
this.renderFrame()
lastTime = time
}
this.rafId = requestAnimationFrame(tick)
}
this.rafId = requestAnimationFrame(tick)
}
destroy() {
if (this.rafId) cancelAnimationFrame(this.rafId)
}
}
Timeline
CSS glitch on text with hover — 4–6 hours. Chromatic aberration for images + JS trigger — 1–2 days. Canvas glitch with row shifting — 2–3 days.







