Glitch text is a fast way to give headings a digital, broken-screen vibe without images or JavaScript. By the end of this tutorial you will build a reusable, themable glitch effect powered by pure CSS. You will control colors with custom properties, duplicate the text using pseudo-elements, slice it with clip-path, and animate the layers for that classic RGB misalignment you see in glitch art.
Why Glitch Text Matters
Glitching sells motion and energy even when nothing else on the page moves. It works in hero banners, promo cards, and loading states. Because it is just CSS, it loads instantly, scales crisply at any size, and stays easy to theme. You will not need images, canvas, or any heavy runtime. You can clone the same component across pages and only tweak a few variables to match the palette of each section.
Prerequisites
You only need a basic understanding of HTML and some comfort with modern CSS. We will lean on custom properties for theming and pseudo-elements to create extra layers without extra markup.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
Keep the markup lean. One element holds the visible text. Two pseudo-elements will duplicate it for the colored channels. Using data-text lets us mirror the content inside CSS without repeating it in the DOM.
<main class="demo-area">
<h1 class="glitch" data-text="GLITCH">GLITCH</h1>
</main>
Step 2: The Basic CSS & Styling
Start with a centered stage, a dark background, and a compact theming system using custom properties. The variables define the base text color along with the red and cyan channel tints. These will drive the duotone split that sells the glitch.
/* CSS */
:root {
--bg: #0e0f12;
--text: #e7e9ee;
--accent-red: #ff003c;
--accent-cyan: #00e6ff;
--intensity: 1; /* 0..1, scales the offset and shake */
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
}
.demo-area {
min-height: 100%;
display: grid;
place-items: center;
padding: 4rem 1rem;
}
.glitch {
position: relative;
display: inline-block;
font-size: clamp(2.8rem, 6vw, 8rem);
line-height: 1;
font-weight: 800;
letter-spacing: 0.03em;
text-transform: uppercase;
/* Slight glow to boost legibility on dark bg */
text-shadow: 0 0 0.04em rgba(231, 233, 238, 0.5);
}
Advanced Tip: Use CSS variables for colors and intensity so you can drop this component onto a light theme, invert the palette, or dial down the shake for calmer contexts. You can also map –intensity to a UI control later for live tuning.
Step 3: Building the Static Glitch Layers
The glitch effect starts by cloning the text into two layers: one tinted red, the other cyan. Offset them slightly on the x-axis. When they move independently, your eye reads a misaligned RGB split. We do this with ::before and ::after and the attr(data-text) trick.
/* CSS */
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
inset: 0;
/* Keep them on top but behind any future overlays */
z-index: 1;
/* Make sure mouse interactions hit the main element, not layers */
pointer-events: none;
/* Use current font styles automatically */
font: inherit;
}
.glitch::before {
color: var(--accent-red);
transform: translateX(calc(1px * var(--intensity)));
/* A crisp edge helps the channel read as a separate layer */
text-shadow: -0.5px 0 0 var(--accent-red);
mix-blend-mode: screen;
}
.glitch::after {
color: var(--accent-cyan);
transform: translateX(calc(-1px * var(--intensity)));
text-shadow: 0.5px 0 0 var(--accent-cyan);
mix-blend-mode: screen;
}
How This Works (Code Breakdown)
Both pseudo-elements copy the original text through content: attr(data-text). This keeps the HTML semantic and avoids duplication. They sit in the same box as the main text because we use position: absolute with inset: 0. That glue makes alignment trivial and reduces layout work.
The color choice mirrors the classic chromatic aberration: one layer in red, one in cyan. Each gets a tiny horizontal offset using transform: translateX. The offsets are multiplied by –intensity, letting you scale the effect globally. mix-blend-mode: screen blends the colored layers with the base text to brighten their overlap. Using text-shadow adds a crisp edge that reads like a misaligned channel on a CRT.
At this point the text already looks a little broken. Many designs stop here for a subtle effect. To push the look, you need slices that jump at different speeds. You can also pair the effect with geometric debris around the headline. For example, if you want jagged shards, build each shard as a CSS triangle and animate it with a mild shake around the text. For horizontal bars that cut through the word, a simple CSS rectangle behind the heading does the job.
Step 4: Animating the Slices
We will use clip-path on the pseudo-elements to show only horizontal slices, changing the slice rectangles over time. Each channel runs its own keyframes so they never align perfectly. Small transforms at each frame sell the jitter.
/* CSS */
@keyframes glitch-clip-red {
0% { clip-path: polygon(0 2%, 100% 2%, 100% 8%, 0 8%); transform: translate(1px, 0) skewX(0.5deg); }
10% { clip-path: polygon(0 15%, 100% 15%, 100% 22%, 0 22%); transform: translate(-1px, -1px) skewX(-0.4deg); }
20% { clip-path: polygon(0 35%, 100% 35%, 100% 41%, 0 41%); transform: translate(2px, 0) skewX(0.6deg); }
30% { clip-path: polygon(0 58%, 100% 58%, 100% 65%, 0 65%); transform: translate(-2px, 1px) skewX(-0.6deg); }
40% { clip-path: polygon(0 75%, 100% 75%, 100% 82%, 0 82%); transform: translate(1px, -1px) skewX(0.2deg); }
50% { clip-path: polygon(0 5%, 100% 5%, 100% 12%, 0 12%); transform: translate(-1px, 0) skewX(-0.2deg); }
60% { clip-path: polygon(0 28%, 100% 28%, 100% 34%, 0 34%); transform: translate(2px, 0) skewX(0.4deg); }
70% { clip-path: polygon(0 48%, 100% 48%, 100% 54%, 0 54%); transform: translate(-2px, 1px) skewX(-0.4deg); }
80% { clip-path: polygon(0 68%, 100% 68%, 100% 74%, 0 74%); transform: translate(1px, -1px) skewX(0.3deg); }
90% { clip-path: polygon(0 85%, 100% 85%, 100% 92%, 0 92%); transform: translate(-1px, 0) skewX(-0.3deg); }
100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); transform: translate(0, 0) skewX(0deg); }
}
@keyframes glitch-clip-cyan {
0% { clip-path: polygon(0 10%, 100% 10%, 100% 16%, 0 16%); transform: translate(-1px, 0) skewX(-0.5deg); }
12% { clip-path: polygon(0 32%, 100% 32%, 100% 38%, 0 38%); transform: translate(1px, 1px) skewX(0.5deg); }
24% { clip-path: polygon(0 55%, 100% 55%, 100% 62%, 0 62%); transform: translate(-2px, -1px) skewX(-0.6deg); }
36% { clip-path: polygon(0 70%, 100% 70%, 100% 76%, 0 76%); transform: translate(2px, 0) skewX(0.4deg); }
48% { clip-path: polygon(0 20%, 100% 20%, 100% 26%, 0 26%); transform: translate(-1px, 0) skewX(-0.2deg); }
60% { clip-path: polygon(0 40%, 100% 40%, 100% 47%, 0 47%); transform: translate(1px, -1px) skewX(0.3deg); }
72% { clip-path: polygon(0 60%, 100% 60%, 100% 66%, 0 66%); transform: translate(-2px, 1px) skewX(-0.3deg); }
84% { clip-path: polygon(0 80%, 100% 80%, 100% 86%, 0 86%); transform: translate(2px, 0) skewX(0.2deg); }
100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); transform: translate(0, 0) skewX(0deg); }
}
/* Apply animations to the cloned layers */
.glitch::before {
animation: glitch-clip-red calc(1.6s + 0.1s * var(--intensity)) steps(20, end) infinite;
}
.glitch::after {
animation: glitch-clip-cyan calc(1.9s + 0.1s * var(--intensity)) steps(24, end) infinite;
}
/* A subtle overall jitter on the main text to bind the effect */
@keyframes glitch-shake {
0% { transform: translate(0, 0); }
25% { transform: translate(0.5px, -0.5px); }
50% { transform: translate(-0.5px, 0.5px); }
75% { transform: translate(0.4px, 0.2px); }
100% { transform: translate(0, 0); }
}
.glitch {
animation: glitch-shake calc(2.2s + 0.2s * var(--intensity)) steps(12, end) infinite;
}
How This Works (Code Breakdown)
Each keyframes block moves a thin horizontal band across the text using clip-path: polygon rectangles. We define several rectangles across the y-axis percentages and then hop between them using steps timing. The result feels erratic without needing JavaScript or per-letter spans.
The red and cyan layers get different timing and clip rectangles so the bands rarely line up. That misalignment creates a convincing twitch. The base element also gets a tiny shake. The shake is slow and uses steps to mirror the choppy feel of the slices. If you want the effect to sit inside a badge or an avatar, wrap it in a shape such as a CSS circle and keep the clipping as-is; the animations are independent.
Advanced Techniques: Adding Animations & Hover Effects
Some interfaces benefit from a calmer default with a burst of glitch on hover or focus. Others need a stronger, pulsing error state. Here are three enhancements: an interactive intensity surge, an idle variant, and a scanline overlay.
/* CSS */
/* 1) Surge intensity on hover/focus */
.glitch {
--intensity: 0.7;
}
.glitch:hover,
.glitch:focus-visible {
--intensity: 1;
filter: saturate(1.05);
}
/* 2) Idle variant that glitches only on interaction */
.glitch.is-idle {
animation: none;
}
.glitch.is-idle::before,
.glitch.is-idle::after {
animation: none;
}
.glitch.is-idle:hover,
.glitch.is-idle:focus-visible {
animation: glitch-shake 2s steps(12, end) infinite;
}
.glitch.is-idle:hover::before,
.glitch.is-idle:focus-visible::before {
animation: glitch-clip-red 1.5s steps(20, end) infinite;
}
.glitch.is-idle:hover::after,
.glitch.is-idle:focus-visible::after {
animation: glitch-clip-cyan 1.8s steps(24, end) infinite;
}
/* 3) Soft scanlines overlay using text-fill and a mask */
.glitch {
position: relative;
isolation: isolate;
}
.glitch::selection {
background: transparent;
color: var(--text);
}
.glitch::after,
.glitch::before {
isolation: isolate; /* keep blending contained */
}
/* Add a very subtle overlay using a new pseudo-element via a helper class */
.glitch.scanlines {
background:
repeating-linear-gradient(
to bottom,
rgba(255,255,255,0.025) 0 2px,
rgba(255,255,255,0.0) 2px 4px
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.glitch.scanlines::before,
.glitch.scanlines::after {
/* Restore color for the channels when text is transparent */
-webkit-text-fill-color: initial;
}
The hover/focus styles crank up –intensity and gently increase color saturation. The .is-idle modifier turns off all animation until interaction. This helps in layouts where motion should not draw attention until the user gets near the control. The scanlines trick uses background-clip: text to paint a repeating gradient inside the glyphs. Because we set color: transparent, the base text shows the lines while the channels still render through their own color context.
Accessibility & Performance
Even flashy text should respect users and CPU. Keep the DOM lean, prefer transform-based movement, and provide a quiet route for users who prefer less motion.
Accessibility
Treat the glitch as decoration unless it conveys meaning. Here the content is the word itself, so the pseudo-elements stay aria-hidden by default. Because we reused the same text through attr(data-text), screen readers will announce it once, which is what we want.
Honor reduced motion settings. Offer a calmer preset that disables the slice jumps while leaving a subtle static split. Below is a drop-in override. Pair it with .is-idle to keep the UI peaceful until interaction.
/* CSS */
@media (prefers-reduced-motion: reduce) {
.glitch,
.glitch::before,
.glitch::after {
animation: none !important;
transform: none !important;
}
.glitch::before {
transform: translateX(0.5px);
}
.glitch::after {
transform: translateX(-0.5px);
}
}
Performance
clip-path and transform are GPU-friendly on modern engines. The animations use steps timing to keep frame churn low. Avoid heavy box-shadow blur or filters on every frame. If the effect will appear in many places on one page, switch to the .is-idle pattern and activate it only on interaction. Also keep font weight and size appropriate for the device; an enormous 900 weight at 12rem costs more to rasterize than a normal headline.
Polish That Pays Off
You built a clean, reusable glitch text effect with only CSS. The component uses pseudo-elements for RGB layers, clip-path for slices, and a small set of variables for theming and control. You can now drop this headline into any layout or wrap it with shape accents like triangles, bars, or badges.
Push it further by binding –intensity to a dataset value per page section, or by mixing it with geometric frames. With this foundation, you have the tools to craft glitch titles that fit your brand rather than a one-size-fits-all preset.