Create a Glitch Text Effect in React
Apply a digital distortion glitch effect to text using CSS clip-path layers and keyframe animation, no external library needed.
Glitch effects tap into something visceral. They look like a system failing, a reality breaking, a signal corrupting. That visual tension is exactly why they work so well in design: controlled chaos is compelling in a way that perfect polish cannot always match.
The final result
GlitchText renders text with two pseudo-element layers that show different horizontal slices of the text, offset in opposite directions and colored red and blue. CSS keyframe animations shift the clip boundaries rapidly, creating the scan-line glitch effect.
Setting up
No animation library. This is pure CSS injected at runtime.
import { useEffect, useRef } from 'react';Building the component
The component creates a unique ID per instance so multiple GlitchText components on the same page do not share styles and interfere with each other.
const style = document.createElement('style');
const id = `glitch-${Math.random().toString(36).slice(2, 8)}`;
container.setAttribute('data-glitch-id', id);The CSS uses ::before and ::after pseudo-elements on the container. Both are positioned absolutely over the text and use content: attr(data-text) to duplicate the text content. The data-text attribute is set directly on the container div.
return (
<div ref={containerRef} data-text={text} className={className}>
{text}
</div>
);The injected styles set up the animation. The ::before layer shows the upper portion of the text (via clip-path: inset(0 0 60% 0)) in red, shifted slightly left. The ::after layer shows the lower portion in blue, shifted right.
style.textContent = `
[data-glitch-id="${id}"]::before {
color: #ff0000;
animation: glitch-before-${id} ${speed}ms infinite linear alternate-reverse;
clip-path: inset(0 0 60% 0);
${enableShadow ? 'text-shadow: -2px 0 #ff0000;' : ''}
}
[data-glitch-id="${id}"]::after {
color: #0000ff;
animation: glitch-after-${id} ${speed}ms infinite linear alternate-reverse;
clip-path: inset(40% 0 0 0);
${enableShadow ? 'text-shadow: 2px 0 #0000ff;' : ''}
}
`;The keyframes shift the clip boundaries and the transform: translate offset at multiple points across the animation, creating an irregular flickering rather than a smooth oscillation.
@keyframes glitch-before-${id} {
0% { clip-path: inset(0 0 80% 0); transform: translate(-2px, -1px); }
20% { clip-path: inset(20% 0 60% 0); transform: translate(2px, 1px); }
40% { clip-path: inset(40% 0 40% 0); transform: translate(-1px, 2px); }
60% { clip-path: inset(60% 0 20% 0); transform: translate(1px, -2px); }
80% { clip-path: inset(10% 0 70% 0); transform: translate(-2px, 1px); }
100% { clip-path: inset(30% 0 50% 0); transform: translate(2px, -1px); }
}The style element is appended to document.head and cleaned up in the effect's return function, so there is no style leak when the component unmounts.
How to use it
<GlitchText
text="SYSTEM ERROR"
speed={500}
enableShadow={true}
className="text-6xl font-black text-white"
/>Lower speed values mean faster, more frantic glitching. A speed of 200ms or less looks genuinely broken; 500-800ms is a bit more controlled.
Key takeaways
- Generating a unique ID per component instance and using attribute selectors to scope the CSS prevents style collisions between multiple glitch elements on the page.
content: attr(data-text)on pseudo-elements is the standard trick for duplicating text content without JavaScript. The parent element must have thedata-textattribute set to the same string.- Cleaning up the injected
<style>element on unmount is important. Without it, stale keyframe definitions accumulate in the document head over time.