26 CSS Link Effects20 / 26

Pure CSSMIT licensed

CSS Glitch Text Hover Animation

Signal corruption on demand: hover and the link splits into magenta/cyan ghosts, horizontal slices tear out of register, and the whole word jitters like a bad transmission — then snaps pristine the instant you leave. Three intensities: chromatic shift only, slice tearing, and full skew-jitter with a rolling scanline band.

Published Updated

Live Demo
Try it

The code

<section class="cle-20" aria-label="Glitch text hover demo">
  <div class="cle-20__stage">
    <div class="cle-20__var"><a href="#a" class="cle-20__rgb" data-text="SYSTEM://LOG">SYSTEM://LOG</a><small class="cle-20__cap">A · chromatic split</small></div>
    <div class="cle-20__var"><a href="#b" class="cle-20__slice" data-text="NIGHT_CITY">NIGHT_CITY</a><small class="cle-20__cap">B · slice tear</small></div>
    <div class="cle-20__var"><a href="#c" class="cle-20__full" data-text="ENTER GRID">ENTER GRID</a><small class="cle-20__cap">C · jitter + scanline</small></div>
  </div>
</section>
.cle-20,
.cle-20 *,
.cle-20 *::before,
.cle-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-20 {
  width: 100%;
  min-height: 100vh;
  --r: oklch(0.65 0.26 350);
  --b: oklch(0.8 0.15 210);
  font-family: ui-monospace,'Cascadia Code',Menlo,Consolas,monospace;
}

.cle-20__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 48px;
  background: linear-gradient(180deg,oklch(0.17 0.02 300),oklch(0.13 0.015 280));
  padding: 44px 32px;
}

.cle-20__var {
  display: grid;
  gap: 14px;
  justify-items: center;
}

.cle-20__cap {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 300);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-20 a {
  position: relative;
  color: oklch(0.93 0.01 300);
  text-decoration: none;
  font-size: clamp(24px,3.4vw,32px);
  font-weight: 700;
  letter-spacing: .08em;
}

.cle-20 a:focus-visible {
  outline: 2px solid var(--b);
  outline-offset: 6px;
}

.cle-20 a::before,
.cle-20 a::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.cle-20 a::before {
  color: var(--r);
  mix-blend-mode: screen;
}

.cle-20 a::after {
  color: var(--b);
  mix-blend-mode: screen;
}

.cle-20__rgb:hover::before,
.cle-20__rgb:focus-visible::before {
  opacity: 1;
  animation: clh20-jitA .3s steps(1,end) infinite;
}

.cle-20__rgb:hover::after,
.cle-20__rgb:focus-visible::after {
  opacity: 1;
  animation: clh20-jitB .3s steps(1,end) infinite;
}

@keyframes clh20-jitA {
  0% {
    translate: -2px -1px;
  }

  50% {
    translate: -3px 1px;
  }

  100% {
    translate: -2px -1px;
  }
}

@keyframes clh20-jitB {
  0% {
    translate: 2px 1px;
  }

  50% {
    translate: 3px -1px;
  }

  100% {
    translate: 2px 1px;
  }
}

.cle-20__slice:hover::before,
.cle-20__slice:focus-visible::before {
  opacity: 1;
  translate: -2px 0;
}

.cle-20__slice:hover::after,
.cle-20__slice:focus-visible::after {
  opacity: 1;
  animation: clh20-slice .55s steps(1,end) infinite;
}

@keyframes clh20-slice {
  0% {
    clip-path: inset(12% 0 78% 0);
    translate: -3px 0;
  }

  25% {
    clip-path: inset(64% 0 22% 0);
    translate: 3px 0;
  }

  50% {
    clip-path: inset(32% 0 56% 0);
    translate: -2px 0;
  }

  75% {
    clip-path: inset(80% 0 6% 0);
    translate: 2px 0;
  }

  100% {
    clip-path: inset(12% 0 78% 0);
    translate: -3px 0;
  }
}

.cle-20__full {
  isolation: isolate;
  overflow: hidden;
  padding: 6px 10px;
}

.cle-20__full::before {
  inset: 6px 10px;
}

.cle-20__full::before {
  inset: 6px 10px;
}

.cle-20__full:hover,
.cle-20__full:focus-visible {
  animation: clh20-skew .5s steps(1,end) infinite;
}

.cle-20__full:hover::before,
.cle-20__full:focus-visible::before {
  opacity: 1;
  animation: clh20-jitA .3s steps(1,end) infinite;
}

.cle-20__full:hover::after,
.cle-20__full:focus-visible::after {
  content: "";
  inset: auto 0;
  height: 22%;
  background: linear-gradient(oklch(1 0 0 / .12),oklch(1 0 0 / .02));
  mix-blend-mode: screen;
  opacity: 1;
  animation: clh20-scan .7s linear infinite;
}

@keyframes clh20-skew {
  0% {
    transform: skewX(0);
  }

  30% {
    transform: skewX(-1.5deg);
  }

  60% {
    transform: skewX(1deg);
  }

  100% {
    transform: skewX(0);
  }
}

@keyframes clh20-scan {
  from {
    top: -24%;
  }

  to {
    top: 104%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-20 a,
    .cle-20 a::before,
    .cle-20 a::after {
    animation: none !important;
  }

  .cle-20 a:hover,
    .cle-20 a:focus-visible {
    color: var(--b);
  }

  .cle-20 a:hover::before,
    .cle-20 a:hover::after {
    opacity: 0;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Link Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Glitch Text Hover Animation
Source: https://codefronts.com/motion/css-link-effects/glitch-split/

Signal corruption on demand: hover and the link splits into magenta/cyan ghosts, horizontal slices tear out of register, and the whole word jitters like a bad transmission — then snaps pristine the instant you leave. Three intensities: chromatic shift only, slice tearing, and full skew-jitter with a rolling scanline band.
## HTML
```html
<section class="cle-20" aria-label="Glitch text hover demo">
  <div class="cle-20__stage">
    <div class="cle-20__var"><a href="#a" class="cle-20__rgb" data-text="SYSTEM://LOG">SYSTEM://LOG</a><small class="cle-20__cap">A · chromatic split</small></div>
    <div class="cle-20__var"><a href="#b" class="cle-20__slice" data-text="NIGHT_CITY">NIGHT_CITY</a><small class="cle-20__cap">B · slice tear</small></div>
    <div class="cle-20__var"><a href="#c" class="cle-20__full" data-text="ENTER GRID">ENTER GRID</a><small class="cle-20__cap">C · jitter + scanline</small></div>
  </div>
</section>
```
## CSS
```css
.cle-20,
.cle-20 *,
.cle-20 *::before,
.cle-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-20 {
  width: 100%;
  min-height: 100vh;
  --r: oklch(0.65 0.26 350);
  --b: oklch(0.8 0.15 210);
  font-family: ui-monospace,'Cascadia Code',Menlo,Consolas,monospace;
}

.cle-20__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 48px;
  background: linear-gradient(180deg,oklch(0.17 0.02 300),oklch(0.13 0.015 280));
  padding: 44px 32px;
}

.cle-20__var {
  display: grid;
  gap: 14px;
  justify-items: center;
}

.cle-20__cap {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 300);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-20 a {
  position: relative;
  color: oklch(0.93 0.01 300);
  text-decoration: none;
  font-size: clamp(24px,3.4vw,32px);
  font-weight: 700;
  letter-spacing: .08em;
}

.cle-20 a:focus-visible {
  outline: 2px solid var(--b);
  outline-offset: 6px;
}

.cle-20 a::before,
.cle-20 a::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.cle-20 a::before {
  color: var(--r);
  mix-blend-mode: screen;
}

.cle-20 a::after {
  color: var(--b);
  mix-blend-mode: screen;
}

.cle-20__rgb:hover::before,
.cle-20__rgb:focus-visible::before {
  opacity: 1;
  animation: clh20-jitA .3s steps(1,end) infinite;
}

.cle-20__rgb:hover::after,
.cle-20__rgb:focus-visible::after {
  opacity: 1;
  animation: clh20-jitB .3s steps(1,end) infinite;
}

@keyframes clh20-jitA {
  0% {
    translate: -2px -1px;
  }

  50% {
    translate: -3px 1px;
  }

  100% {
    translate: -2px -1px;
  }
}

@keyframes clh20-jitB {
  0% {
    translate: 2px 1px;
  }

  50% {
    translate: 3px -1px;
  }

  100% {
    translate: 2px 1px;
  }
}

.cle-20__slice:hover::before,
.cle-20__slice:focus-visible::before {
  opacity: 1;
  translate: -2px 0;
}

.cle-20__slice:hover::after,
.cle-20__slice:focus-visible::after {
  opacity: 1;
  animation: clh20-slice .55s steps(1,end) infinite;
}

@keyframes clh20-slice {
  0% {
    clip-path: inset(12% 0 78% 0);
    translate: -3px 0;
  }

  25% {
    clip-path: inset(64% 0 22% 0);
    translate: 3px 0;
  }

  50% {
    clip-path: inset(32% 0 56% 0);
    translate: -2px 0;
  }

  75% {
    clip-path: inset(80% 0 6% 0);
    translate: 2px 0;
  }

  100% {
    clip-path: inset(12% 0 78% 0);
    translate: -3px 0;
  }
}

.cle-20__full {
  isolation: isolate;
  overflow: hidden;
  padding: 6px 10px;
}

.cle-20__full::before {
  inset: 6px 10px;
}

.cle-20__full::before {
  inset: 6px 10px;
}

.cle-20__full:hover,
.cle-20__full:focus-visible {
  animation: clh20-skew .5s steps(1,end) infinite;
}

.cle-20__full:hover::before,
.cle-20__full:focus-visible::before {
  opacity: 1;
  animation: clh20-jitA .3s steps(1,end) infinite;
}

.cle-20__full:hover::after,
.cle-20__full:focus-visible::after {
  content: "";
  inset: auto 0;
  height: 22%;
  background: linear-gradient(oklch(1 0 0 / .12),oklch(1 0 0 / .02));
  mix-blend-mode: screen;
  opacity: 1;
  animation: clh20-scan .7s linear infinite;
}

@keyframes clh20-skew {
  0% {
    transform: skewX(0);
  }

  30% {
    transform: skewX(-1.5deg);
  }

  60% {
    transform: skewX(1deg);
  }

  100% {
    transform: skewX(0);
  }
}

@keyframes clh20-scan {
  from {
    top: -24%;
  }

  to {
    top: 104%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-20 a,
    .cle-20 a::before,
    .cle-20 a::after {
    animation: none !important;
  }

  .cle-20 a:hover,
    .cle-20 a:focus-visible {
    color: var(--b);
  }

  .cle-20 a:hover::before,
    .cle-20 a:hover::after {
    opacity: 0;
  }
}
```

How this works

The ghosts are the link's own data-text rendered twice on stacked pseudo-elements, tinted and blended. The tearing is clip-path jumping between horizontal bands with steps(1, end) timing — glitches don't ease, they jump:

.slice:hover::after{ animation: clh20-slice .55s steps(1,end) infinite; }
@keyframes clh20-slice{
  0%  { clip-path: inset(12% 0 78% 0); translate: -3px 0; }
  25% { clip-path: inset(64% 0 22% 0); translate:  3px 0; }
  50% { clip-path: inset(32% 0 56% 0); translate: -2px 0; }
  75% { clip-path: inset(80% 0  6% 0); translate:  2px 0; }
}

Each frame clips a different thin band of the duplicate and shoves it sideways; holding values between frames (steps) is what sells the digital-error feel. A keeps only the two color ghosts drifting ±2px with mix-blend-mode:screen, so overlapping ink stays white — true chromatic aberration. C adds a two-frame skew jitter on the link itself plus a translucent scanline band (a gradient pseudo-element) rolling top to bottom on a loop.

Make it yours

  • Violence dial: the ±2–3px translate amplitudes and the .55s loop — halve the duration for panic mode.
  • Ghost hues: --r/--b tokens (magenta/cyan reads 'RGB split'; red/blue reads VHS).
  • Slice pattern: re-roll the inset() band positions; keep 4–6 frames so the loop doesn't read as a cycle.
  • Trigger it elsewhere: move the animations to a .is-glitching class and fire it from JS on route errors.

Gotchas — read before shipping

  • Duplicated text must come from attr(data-text) — keep the attribute in sync with the label or the ghosts spell a different word.
  • inset:0 pins ghosts to the padding box — on a padded link the duplicates render shifted up-left of the real text. Match the inset to the padding (inset:6px 10px here) or keep glitch links padding-free.
  • Jitter + slices are photosensitivity-adjacent: amplitudes here stay small and the demo disables ALL of it under prefers-reduced-motion, leaving a plain color change.
  • mix-blend-mode blends with what's behind it: A sits on a solid dark stage; on photos, wrap the link in its own background or the ghosts vanish.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

clip-path animation, blend modes and steps() are Baseline everywhere; oklch sets the stated floor.

Search CodeFronts

Loading…