Learn how to create a glitch effect with pure CSS, on text, images, buttons and more. Customize and copy the code.
Hover over the preview to trigger the glitch. Adjust intensity, speed, and color channels below, then copy the generated CSS.
Text Glitch Effect
Distort headings and text with clip-path slicing and color-shifted pseudo-elements
The CSS version above works for hover and click interactions. But imagine this glitch effect playing between fullscreen sections as users scroll through your website: pixel-level distortion, chromatic aberration, and slice offsets all running on the GPU at 60 fps.
Scroll inside the preview below to see the WebGL glitch transition in action. Adjust the speed to find the right feel:
fullPage.js adds
Scroll inside the preview to see the glitch transition between sections
A glitch effect is a visual technique that simulates digital or analog signal corruption, the kind of distortion you see when a screen malfunctions, a VHS tape degrades, or a data stream is interrupted. In web development, it refers to deliberately broken-looking animations applied to text, images, or entire sections to create a raw, edgy aesthetic.
The glitch aesthetic has its roots in glitch art, a movement that emerged in the late 1990s and early 2000s where artists intentionally corrupted digital files or manipulated hardware to produce unexpected visual artifacts. Inspired by CRT monitor failures, VHS tracking errors, and corrupted JPEG data, the style became a staple of cyberpunk, vaporwave, and electronic music culture. It eventually made its way into web design as CSS animations and blend modes became powerful enough to replicate the look natively in the browser.
The CSS glitch effect is versatile and can be applied to virtually any HTML element:
Glitch effects work best when the design calls for an unconventional, high-energy feel. Common use cases include:
Use the effect sparingly. A glitch animation on a single heading makes a strong statement, but applying it to every element on the page can quickly become overwhelming and hurt readability.
The text glitch uses two pseudo-elements (::before and ::after) that duplicate the heading via content: attr(data-text). Each copy is tinted a different colour, cyan and magenta by default, to simulate RGB channel separation. The distortion comes from clip-path: inset(), which crops each pseudo-element to a thin horizontal strip, while transform: translate() shifts them off-axis:
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0; left: 0;
}
.glitch::before { color: #0ff; }
.glitch::after { color: #f0f; }
@keyframes glitch-1 {
0% { clip-path: inset(20% 0 60% 0); transform: translate(4px, -2px); }
50% { clip-path: inset(40% 0 30% 0); transform: translate(-4px, 2px); }
}
The key to making it feel authentically digital is steps(2) or steps(3) in the animation timing function. This removes easing entirely: each frame snaps to the next position, mimicking the way real signal corruption jumps rather than slides. Combine that with a short duration (0.2–0.4s) and infinite iteration for a continuous flicker, or restrict it to :hover to keep the page calm until the user interacts.
Browser support is excellent: clip-path: inset(), pseudo-elements, and @keyframes work in every modern browser. No JavaScript, no polyfills.
The WebGL approach runs the entire glitch pipeline on the GPU through a custom fragment shader. Two textures are loaded into a <canvas> element via WebGL, and the shader reads both during each frame. The image is divided into horizontal slices, each offset by a random amount, with a chromatic aberration pass that samples RGB channels at different UV coordinates:
// Fragment shader — slice offset + chromatic split
float slice = floor(uv.y * sliceCount);
float offset = random(slice + time) * maxOffset;
float r = texture2D(image, vec2(uv.x + offset + chromatic, uv.y)).r;
float g = texture2D(image, vec2(uv.x + offset, uv.y)).g;
float b = texture2D(image, vec2(uv.x + offset - chromatic, uv.y)).b;
gl_FragColor = vec4(r, g, b, 1.0);
Because everything is processed inside the GPU pipeline, there is no DOM manipulation and no reflow. The animation loop uses requestAnimationFrame and updates a single uniform float (the glitch progress) that the shader interpolates. This makes it trivially smooth even at high slice counts.
The trade-off vs pure CSS: WebGL requires images served from the same origin or a CORS-enabled CDN, since the shader reads raw pixel data via texImage2D. The result, however, is far more flexible: you get real per-pixel displacement, not just clipped layers.
The image glitch applies the same pseudo-element strategy as text, but adapted for raster content. The container inherits its background to both ::before and ::after, each tinted with mix-blend-mode: multiply to produce colour-shifted copies:
.glitch-img::before,
.glitch-img::after {
content: '';
position: absolute;
inset: 0;
background: inherit;
background-size: cover;
mix-blend-mode: multiply;
}
.glitch-img::before { background-color: #0ff; }
.glitch-img::after { background-color: #f0f; }
.glitch-img:hover::before {
animation: glitch-img 0.3s infinite steps(3);
}
On hover, both pseudo-elements animate using clip-path: inset() to show only a horizontal slice at a time, while transform: translateX() offsets them in opposite directions. The steps(3) timing gives it a jumpy, corrupted-signal feel. The original image stays solid underneath; only the coloured layers shift.
The presets in the generator above change the number of visible slices, the blend mode, and the offset range. "Scanline" uses narrow insets for thin strips; "Heavy" uses wider insets and larger translations; "Neon" swaps the tints for bright contrasting colours.
The scanline technique stacks multiple copies of the same background image, five layers by default, inside a single container. Each layer is a <div> positioned absolutely to fill the parent, oversized by a gap value to create thin transparent scanlines:
.scanline__img {
position: absolute;
top: calc(-1 * var(--gap-v));
left: calc(-1 * var(--gap-h));
width: calc(100% + var(--gap-h) * 2);
height: calc(100% + var(--gap-v) * 2);
background: url('photo.jpg') no-repeat center / cover;
}
.scanline__img:nth-child(2) {
background-color: #f3727e;
background-blend-mode: color-dodge;
}
Each layer is animated with a different @keyframes sequence that changes transform: translate() and clip-path at staggered intervals. Layers 2+ are tinted with background-blend-mode (colour-dodge, luminosity, etc.) to create colour interference between them. The result looks like a broken interlaced signal: horizontal bands of colour drifting at different speeds.
The direction control switches between horizontal and vertical scanlines by rotating the background-size axis and the translate direction. Blend mode and colour controls per layer give fine-grained control over the interference pattern.
The colour shift (chromatic aberration) effect creates a pseudo-element copy of an image with mix-blend-mode: hard-light, then jitters its position through a keyframe sequence. Because the layer is offset by only a few pixels, the visual result is a subtle colour fringe around every edge, the same artifact real camera lenses produce at wide apertures:
.color-shift::before {
content: '';
position: absolute;
inset: 0;
background: inherit;
opacity: .5;
mix-blend-mode: hard-light;
}
@keyframes color-shift {
0% { background-position: 0 0; filter: hue-rotate(0deg); }
30% { background-position: 15px 0; }
60% { background-position: -50px 0; }
100%{ background-position: 0 0; filter: hue-rotate(360deg); }
}
At low intensity (1–3px) the effect looks like a lens defect; at high intensity (10–20px) it becomes a full chromatic split. The steps() timing function makes the movement snap rather than slide.
This is the most versatile variant for production use: it works well as a subtle hover state on cards or hero images without overwhelming the design.
The RGB split takes a literal approach: three copies of the same image are stacked inside a container, each filtered by an SVG <feColorMatrix> to isolate a single colour channel. The three layers are composited with mix-blend-mode: screen, which is additive, so R + G + B reconstructs the original colour:
<!-- SVG filters to isolate each channel -->
<filter id="red">
<feColorMatrix values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
</filter>
<filter id="green">
<feColorMatrix values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
</filter>
<!-- CSS -->
.rgb-red { filter: url(#red); mix-blend-mode: screen; }
.rgb-green { filter: url(#green); mix-blend-mode: screen; }
.rgb-blue { filter: url(#blue); mix-blend-mode: screen; }
On hover, each channel is animated to translate a few pixels in a different direction. Because the layers are additively blended, the offset creates true anaglyph-style colour separation: reds shift left, blues shift right, and green stays centred (or vice versa). The effect is identical to what you see through misaligned 3D glasses.
Unlike the pseudo-element approaches, this method gives you independent control over each colour channel. The trade-off is three <img> elements in the DOM instead of CSS-only pseudo-elements, plus the inline SVG filter definitions.
The button glitch positions two pseudo-elements behind the button using z-index: -1. Each is a full copy of the button's dimensions, tinted cyan and magenta. In the resting state, both sit exactly behind the button and are invisible:
.glitch-btn::before,
.glitch-btn::after {
content: '';
position: absolute;
inset: 0;
}
.glitch-btn::before { background: #0ff; z-index: -1; }
.glitch-btn::after { background: #f0f; z-index: -2; }
.glitch-btn:hover::before {
animation: btn-jitter 0.3s ease 0.1s infinite;
}
.glitch-btn:hover::after {
animation: btn-jitter 0.3s ease infinite reverse;
}
On :hover, both pseudo-elements animate with transform: translate(), creating coloured slices that jitter around the edges of the button. The inner text stays solid and readable; only the coloured "echoes" move. The steps(2) timing function makes the movement instant and digital rather than smooth.
For best results, use a solid background colour on the button so the pseudo-element tints contrast sharply. The effect works on any clickable element: <button>, <a>, or any block with position: relative.
A well-tuned glitch effect feels intentional and polished. A poorly tuned one feels broken. Here are some practical guidelines to get it right.
Small offsets (2-4 px) and short durations (0.2-0.4 s) feel dynamic without being distracting. Reserve high-intensity glitches for hover or transition moments.
Smooth easing looks wrong on a glitch. Real signal corruption is instant. Use steps(2) or steps(3) in your animation timing for that digital snap.
Glitching everything on the page creates visual noise. Apply it to one headline, one hero image, or one CTA button. Let the rest of the page breathe.
Cyan + magenta is the classic RGB split, but you can use brand colours instead. Keep the two tints complementary for the best contrast.
Wrap your glitch animations inside @media (prefers-reduced-motion: no-preference) so users who find motion uncomfortable see a static version.
Layer clip-path slicing with a text-shadow RGB split or add a subtle skew transform. Mixing two lightweight effects often looks richer than cranking one to the max.
Glitch effects add personality to designer and developer portfolio sites, especially for digital art, gaming, or cyberpunk aesthetics.
A glitch effect on the error message reinforces the "something went wrong" concept in a visually interesting way.
Tech product reveals and startup landing pages use glitch transitions to convey innovation and cutting-edge technology.
Concert pages, DJ portfolios, and music streaming interfaces use glitch aesthetics to match the energy of electronic music.
Game landing pages, esports team sites, and retro gaming blogs. Glitch effects fit naturally into the visual language of gaming culture.
Futuristic UI designs, hacker-themed pages, and dystopian storytelling sites use glitch as a core visual element of the world they're building.
::before and ::after pseudo-elements with clip-path: inset() to create horizontal slices, apply different colour tints (cyan and magenta) for RGB channel splitting, and animate with @keyframes using steps() for the jittery, digital look. See the interactive demo above to generate the code.
background-image on the element and its pseudo-elements. Use mix-blend-mode: multiply with colour tints on the pseudo-elements, then animate with clip-path and transform offsets. The image demo tab above shows this technique.
::before with cyan and ::after with magenta) positioned behind the button, then animate them with a translate-based jitter on :hover. The inner content stays solid while the colour layers shift around it. See the "Button Glitch" tab in the generator above.
transform and clip-path are GPU-composited in modern browsers, so they don't trigger layout or paint. A single glitch effect has negligible performance cost. Avoid running dozens of glitch animations simultaneously on the same page.
@media (prefers-reduced-motion: no-preference) query. Users who have enabled "Reduce motion" in their OS settings will see a static version. You can also limit the animation to :hover or :focus instead of running it continuously.
#0ff) and magenta (#f0f) because they mimic RGB channel separation, but you can use any two contrasting colours. For a subtler look, try two shades of the same hue with slight offsets.
.glitch-active) when the element enters the viewport. Move your animation declarations to .glitch-active::before / .glitch-active::after so the effect only plays when visible.
Add scroll transitions, parallax and animations to your site with fullPage.js. One component, 80+ effects.
Powering fullscreen design for Google, Sony, BBC, eBay & more