Dirck Mulder
Text Animations||3 min read

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.

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
@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

tsx
<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 the data-text attribute 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.