Most demos need a newer browser

They use :has() and container queries. Update your browser, or on an iPhone update iOS to 16.4 or later.

CSS Glitch Effect

An animated glitch effect using CSS clip-path, pseudo-elements, and keyframes with no JavaScript, plus hover-triggered and text-shadow variants.

Level
Intermediate
Published
Updated
Reading time
7 min

Try it

3 live demos

Glitch
Base
Glitch
Intense

Two intensities

Other ways to build it

Build it

6 snippets to copy

CSS Glitch Effect

HTML

<div class="glitch-row">
  <div class="glitch-wrapper">
    <span class="glitch" data-text="Glitch">Glitch</span>
  </div>
  <span class="glitch-tag">Base</span>
</div>

<div class="glitch-row">
  <div class="glitch-wrapper">
    <span class="glitch glitch-intense" data-text="Glitch">Glitch</span>
  </div>
  <span class="glitch-tag">Intense</span>
</div>

CSS

/* each glitch sits over a small muted label naming its intensity */
.glitch-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
}

.glitch-tag {
  font-family: "DM Mono", "Fira Code", Consolas, monospace;
  font-size: .7rem;
  color: #88888f;
  text-transform: uppercase;
  letter-spacing: .08em;
}

.glitch-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
}

.glitch {
  position: relative;
  color: #f0f0f0;
  font-family: "DM Mono", "Fira Code", Consolas, monospace;
  font-size: 3rem;
  font-weight: 800;
  letter-spacing: .05em;
}

.glitch::before,
.glitch::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.glitch::before {
  color: #57d9a3;
  animation: glitch-1 1.5s infinite;
}

.glitch::after {
  color: #ff6b6b;
  animation: glitch-2 1.5s infinite;
}

/* Intense version: faster cycle, wider offsets */
.glitch-intense::before { animation: glitch-intense-1 0.8s infinite; }
.glitch-intense::after  { animation: glitch-intense-2 0.8s infinite; }

@keyframes glitch-1 {
  0%, 90%, 100% { clip-path: inset(0 0 100% 0); left: 0; }
  92%  { clip-path: inset(20% 0 60% 0); left: -3px; }
  94%  { clip-path: inset(50% 0 30% 0); left:  2px; }
  96%  { clip-path: inset(10% 0 75% 0); left: -2px; }
  98%  { clip-path: inset(65% 0 10% 0); left:  3px; }
}

@keyframes glitch-2 {
  0%, 88%, 100% { clip-path: inset(0 0 100% 0); left: 0; }
  90%  { clip-path: inset(40% 0 40% 0); left:  3px; }
  92%  { clip-path: inset(5%  0 80% 0); left: -3px; }
  94%  { clip-path: inset(75% 0 5%  0); left:  2px; }
  96%  { clip-path: inset(30% 0 55% 0); left: -2px; }
}

@keyframes glitch-intense-1 {
  0%, 72%, 100% { clip-path: inset(0 0 100% 0); left: 0; }
  74%  { clip-path: inset(10% 0 55% 0); left: -7px; }
  78%  { clip-path: inset(45% 0 20% 0); left:  6px; }
  82%  { clip-path: inset(70% 0 5%  0); left: -5px; }
  86%  { clip-path: inset(5%  0 72% 0); left:  8px; }
  90%  { clip-path: inset(30% 0 40% 0); left: -4px; }
  94%  { clip-path: inset(85% 0 3%  0); left:  5px; }
}

@keyframes glitch-intense-2 {
  0%, 68%, 100% { clip-path: inset(0 0 100% 0); left: 0; }
  71%  { clip-path: inset(60% 0 15% 0); left:  8px; }
  75%  { clip-path: inset(15% 0 60% 0); left: -7px; }
  79%  { clip-path: inset(88% 0 3%  0); left:  5px; }
  83%  { clip-path: inset(3%  0 88% 0); left: -8px; }
  87%  { clip-path: inset(40% 0 35% 0); left:  6px; }
  91%  { clip-path: inset(22% 0 58% 0); left: -5px; }
}

/* without the animation nothing clips the clones, so hide them
   outright or the coral copy covers the real text */
@media (prefers-reduced-motion: reduce) {
  .glitch::before,
  .glitch::after {
    animation: none;
    clip-path: inset(0 0 100% 0);
  }
}

Glitch on hover and focus only

Moving the animation onto :hover and :focus-visible turns a perpetual flicker into a response. Nothing moves until someone points at the link or tabs to it, which removes the accessibility problem and the constant compositing work at the same time. The two pseudo-elements still exist at rest, so they are given an explicit clip-path: inset(0 0 100% 0) outside the animation rather than relying on the first keyframe to hide them. Hover the link below, or tab to it.

HTML

<a href="/enter" class="glitch glitch-hover" data-text="Hover me">Hover me</a>

CSS

/* uses @keyframes glitch-1, @keyframes glitch-2 from the main example above; the rules here are
   what changes, not the whole file */
/* hidden at rest, rather than trusting the first keyframe to do it */
.glitch-hover::before,
.glitch-hover::after {
  clip-path: inset(0 0 100% 0);
  animation: none;
}

.glitch-hover:hover::before,
.glitch-hover:focus-visible::before {
  animation: glitch-1 1.5s infinite;
}

.glitch-hover:hover::after,
.glitch-hover:focus-visible::after {
  animation: glitch-2 1.5s infinite;
}

@media (prefers-reduced-motion: reduce) {
  .glitch-hover:hover::before,
  .glitch-hover:hover::after,
  .glitch-hover:focus-visible::before,
  .glitch-hover:focus-visible::after { animation: none; }
}

Channel split with text-shadow alone

Two colored text-shadow layers offset in opposite directions give the same split channel look with one element, no attribute, and no pseudo-elements. Animating the offsets shakes the two colors apart and back. What it cannot do is tear a horizontal band out of the middle of a word, because a shadow always follows the whole glyph. What it does better is degrade: a browser that renders none of this shows plain readable text rather than three stacked copies.

HTML

<span class="glitch-rgb">Signal</span>

CSS

.glitch-rgb {
  font-family: "DM Mono", Consolas, monospace;
  font-size: 3rem;
  font-weight: 800;
  color: #f0f0f0;
  animation: rgb-split 2.4s infinite;
}

/* the shadows carry the color, so the element itself never moves */
@keyframes rgb-split {
  0%, 86%, 100% {
    text-shadow: 2px 0 #ff6b6b, -2px 0 #57d9a3;
  }
  88% { text-shadow:  7px 0 #ff6b6b,  -7px 0 #57d9a3; }
  90% { text-shadow: -6px 0 #ff6b6b,   6px 0 #57d9a3; }
  92% { text-shadow:  9px 2px #ff6b6b, -9px -2px #57d9a3; }
  94% { text-shadow: -4px 0 #ff6b6b,   4px 0 #57d9a3; }
}

@media (prefers-reduced-motion: reduce) {
  .glitch-rgb {
    animation: none;
    text-shadow: 2px 0 #ff6b6b, -2px 0 #57d9a3;
  }
}

Understand it

How it works and 6 properties

A CSS glitch effect fakes a torn video signal by drawing the same text three times and tearing two of the copies apart. The ::before and ::after pseudo-elements clone the text from a data-text attribute, each takes a different color, and rapidly changing clip-path: inset() slices exposes a different horizontal band of each clone on every keyframe. Nothing moves except the clipping windows and a few pixels of horizontal offset.

The timing is what sells it. Both animations sit idle for most of their cycle and do all their work in the last ten percent, so the text sits still and then tears for a fraction of a second, which is how a real signal fault behaves. Two variants follow: one that only glitches on hover or focus, and one built from text-shadow alone with no pseudo-elements at all.

How it works

Two ::before and ::after pseudo-elements clone the text using content: attr(data-text). Each clone is offset horizontally with left and colored using color. Rapidly switching clip-path: inset() slices inside @keyframes creates the scan-line tearing. The parent uses position:relative to contain the pseudo-elements.

content: attr(data-text) is the piece that keeps the markup honest. The clones read their text from an attribute on the element rather than from a string hard coded in the stylesheet, so one rule serves every glitched heading on the site and the words only exist in one place. The cost is that the text has to be written twice in the HTML, once as content and once in the attribute, and they will drift apart the first time someone edits only one of them.

clip-path: inset(top right bottom left) hides everything outside the rectangle it describes. inset(0 0 100% 0) clips away the full height, which is how both clones stay invisible for the quiet part of the cycle. inset(20% 0 60% 0) leaves a band running from twenty percent down to forty percent, and jumping between bands at different keyframes is what produces the horizontal tearing.

Both clones sit at top: 0; left: 0 over a position: relative parent, so they stack exactly on the original text. The left value is animated by a few pixels in each direction, which offsets a band sideways at the same moment it becomes visible. The teal and coral colors are what make that offset read as a channel split rather than as a blur.

The keyframe percentages are unevenly spaced on purpose. 0%, 90%, 100% all share the hidden state, and the four tearing frames are packed between 92 and 98 percent. Spread the same frames evenly across the cycle and the effect becomes a constant shimmer, which looks like a rendering bug rather than a glitch. The intense variant uses a shorter duration and a wider window, so it fires more often and further.

Because both pseudo-elements are painted copies of text, none of this is visible to the accessibility tree or to a text search. The original element still holds the real content. That also means a browser that ignores clip-path shows three overlapping copies at full opacity, which is the one degradation worth planning for.

Accessibility notes

Rapid flickering is a seizure risk, and the guidance is specific: nothing should flash more than three times per second. The base version fires its tearing frames inside a short window near the end of a 1.5 second cycle, which sits under that limit, and the intense version at 0.8 seconds runs closer to it. Slowing the cycle rather than adding frames is the safer way to make a glitch feel more aggressive.

Anything that runs forever should stop when the system asks for less motion. A prefers-reduced-motion block that sets animation: none on both pseudo-elements is not enough on its own, because the clones then show at full size over the text. The block also needs clip-path: inset(0 0 100% 0) to hide them, which leaves the heading rendered as ordinary text. The hover variant is the better default: motion only happens when someone asks for it.

The duplicated text is invisible to assistive technology, since pseudo-element content generated by CSS is not exposed the way real content is. What does matter is contrast. The colored clones sit over the real text and, during a tear, partially replace it, so the underlying color still has to pass contrast on its own without help from the effect.

Mistakes worth avoiding

  • Letting the data-text attribute drift out of sync with the visible text. The clones say one thing and the element says another, and the mismatch only shows during a tear.
  • Spacing the keyframes evenly. The effect stops looking like a fault and starts looking like a constant shimmer, which reads as a rendering bug.
  • Leaving the parent at position: static. Both clones then position themselves against the nearest positioned ancestor instead, and the copies land somewhere else on the page entirely.
  • Running the animation continuously with no reduced motion escape. Perpetual flicker is a genuine accessibility failure, not a stylistic preference.
  • Assuming the clones are hidden by default. They are only hidden by the clip-path in the first keyframe, so anywhere clip-path does not apply, all three copies are visible at once.

Ship it

4 features in 4 browsers, 5 questions

Browser support

Feature Chrome Firefox Safari Edge
animation455.112
::before content423.112
text-shadow43.5412
prefers-reduced-motion746310.179

clip-path is deliberately absent from the table. Can I Use records it as only partially supported on HTML elements in Chrome, Safari and Edge, so quoting a version number from that entry would be misleading. The partial mark is not about the inset() basic shape this effect uses, which is what every current browser implements. The degradation matters more than the number: a browser that ignores clip-path never hides the two clones, so instead of a glitch you get three overlapping copies of the heading at full opacity. The text-shadow variant avoids that failure mode completely.

What you can build with it

  • Cyberpunk and synthwave headings. The genre the effect belongs to, where a single glitched wordmark sets the tone for a page.
  • Error and 404 pages. A broken looking status code reads as intentional rather than as a template nobody styled.
  • Game and demoscene sites. Title treatments and section labels, usually alongside a monospace face and a scanline overlay.
  • Hover states on links. A brief tear on hover, which is the hover variant, gives a link a strong response without any layout movement.
  • Loading and processing labels. Text that tears while a task runs, paired with something like the typewriter effect to spell the message out first.

Frequently asked questions

How do you make a glitch text effect in CSS?
Put the text in a data-text attribute as well as in the element, clone it into ::before and ::after with content: attr(data-text), stack both copies over the original with absolute positioning, then animate clip-path: inset() and a small left offset so different horizontal bands of each clone appear and shift.
Why is my glitch effect showing three copies of the text?
The clip-path is not being applied. Either the first keyframe is missing its inset(0 0 100% 0) hidden state, or the animation has not started, or the browser is ignoring the property. Adding opacity: 0 to the resting state of both pseudo-elements gives you a second line of defense.
Does the glitch effect need JavaScript?
No, the glitch effect needs no JavaScript. Everything here is @keyframes, two pseudo-elements, and one data attribute. The randomness is faked by choosing irregular keyframe percentages, since CSS has no random function.
Is a glitch animation accessible?
Only if it is controlled. Keep the flashing under three times per second, and honor prefers-reduced-motion by turning the animation off and hiding the clones with clip-path. Triggering the effect on hover or focus rather than running it forever is the safest version, and it is the first variant.
Can you do a glitch effect without pseudo-elements?
Yes, a glitch effect works without pseudo-elements. Two offset text-shadow layers in different colors give the same channel split look, and animating the offsets produces the shake. It is one element and no attributes, it degrades to plain text rather than to three overlapping copies, and it cannot produce the horizontal band tearing that clip-path gives you.