MPD

Matthew Peck-Deloughry

As with all my technical documents, I like to preface the articles, that I don’t pretend to be a master in said topic, nor always do stuff the best way, these articles are here as a documentation of my exploration, and if you find this useful then awesome! If you see something you want to discuss please hit me up on twitter as I’m always up for a good chat!

So… you know how every website these days looks like it was generated by the same Figma template? Clean gradients, rounded corners, Inter font everywhere? Yeah, I got tired of that.

When redesigning my website, I decided to embrace my inner 90s kid and go full CRT-monitor-in-a-dark-room aesthetic. We’re talking scan lines, chromatic aberration, and the kind of visual glitches that would make your IT department nervous.

CRT static glitch effect

“But Matt, won’t that be distracting?” — Look, if you wanted calm and peaceful, you wouldn’t be reading a blog with intentional visual corruption. We’re here to have fun and break things (responsibly, with CSS).

The challenge was making it feel intentional and refined rather than cheap and overused. Because there’s a fine line between “cool retro aesthetic” and “did your website break?”

This is fine

Spoiler: sometimes it did break. But that’s part of the journey!

In this post, I’ll walk through how I built several glitch effects using pure CSS:

  1. CRT scan lines and the oscilloscope effect
  2. Chromatic aberration on images
  3. Text glitch animations
  4. Combining everything into a cohesive system

Let’s get into it!

Retro 80s hacker vibes

The Foundation: CSS Custom Properties

Before diving into the effects, I set up a colour palette specifically for glitch effects. These colours are used consistently across all animations:

:root {
  /* Glitch Colors - for chromatic aberration */
  --glitch-red: #FF4757;
  --glitch-cyan: #00D9FF;

  /* Extended palette for variety */
  --color-pink: #FF6B9D;
  --color-success: #00FF87;
  --color-warning: #FFFA65;
}

The red and cyan combination is classic chromatic aberration - it mimics the RGB separation you see on old CRT monitors or damaged digital displays.

CRT Scan Lines

The most recognisable CRT effect is scan lines - those horizontal lines you see on old televisions. This is surprisingly simple to achieve with a repeating linear gradient:

.crt-filter::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0, 0, 0, 0.25) 2px,
    rgba(0, 0, 0, 0.25) 4px
  );
  pointer-events: none;
  z-index: 10;
  animation: crt-scanline-flicker 0.08s steps(2) infinite;
}

@keyframes crt-scanline-flicker {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.92; }
}

The key bits here:

  • repeating-linear-gradient creates the horizontal lines at 2px intervals
  • steps(2) in the animation creates that stuttery, digital feel rather than smooth transitions
  • The subtle opacity flicker adds life to the effect without being distracting

Making it Work on Images

One gotcha - pseudo-elements (::before, ::after) don’t work on <img> tags since they’re void elements. You have two options:

Option 1: Wrap the image in a container

<div class="crt-filter">
  <img src="your-image.jpg" alt="..." />
</div>

Option 2: Use CSS filters directly on the image

img.crt-image {
  animation: crt-image-glitch 4s steps(1) infinite;
  box-shadow:
    3px 0 0 rgba(255, 71, 87, 0.3),
    -3px 0 0 rgba(0, 217, 255, 0.3);
}

The box-shadow approach gives you chromatic aberration without needing pseudo-elements.

Chromatic Aberration

Chromatic aberration is that colour fringing effect where red and cyan appear offset from the main image. There are several ways to achieve this:

Method 1: Box Shadow (Simple)

img {
  box-shadow:
    2px 0 0 rgba(255, 71, 87, 0.2),
    -2px 0 0 rgba(0, 217, 255, 0.2);
}

This creates subtle red/cyan edges on all images. Simple but effective.

Method 2: Animated Jitter

For more dynamic chromatic aberration, animate the offset:

@keyframes chromatic-jitter {
  0%, 100% {
    box-shadow:
      3px 0 0 rgba(255, 71, 87, 0.3),
      -3px 0 0 rgba(0, 217, 255, 0.3);
  }
  50% {
    box-shadow:
      4px 1px 0 rgba(255, 71, 87, 0.4),
      -4px -1px 0 rgba(0, 217, 255, 0.4);
  }
}

Method 3: Layered Images (Advanced)

For the most realistic effect, layer multiple copies of the same image:

<div className="chromatic-wrapper">
  <img src={image} className="layer-main" />
  <img src={image} className="layer-red" aria-hidden="true" />
  <img src={image} className="layer-cyan" aria-hidden="true" />
</div>
.layer-red {
  position: absolute;
  mix-blend-mode: multiply;
  opacity: 0.6;
  animation: chromatic-red 0.12s steps(2) infinite;
}

.layer-cyan {
  position: absolute;
  mix-blend-mode: screen;
  opacity: 0.5;
  animation: chromatic-cyan 0.15s steps(2) infinite;
}

@keyframes chromatic-red {
  0%, 100% { transform: translate(2px, 1px); }
  50% { transform: translate(3px, 0); }
}

@keyframes chromatic-cyan {
  0%, 100% { transform: translate(-2px, -1px); }
  50% { transform: translate(-3px, 1px); }
}

The Oscilloscope Waveform

One of the more complex effects I built was an animated oscilloscope for my Spotify “Now Playing” widget. This uses SVG path animation:

<svg className="sw-wave" viewBox="0 0 200 40">
  <path
    className="sw-wave-path"
    d="M0,20 Q10,5 20,20 T40,20 T60,20 T80,20 T100,20..."
  />
</svg>

The magic is animating the SVG path’s d attribute:

.sw-wave-path {
  fill: none;
  stroke: var(--color-success);
  stroke-width: 2;
  filter: drop-shadow(0 0 4px var(--color-success));
  animation: wave-morph 0.8s ease-in-out infinite;
}

@keyframes wave-morph {
  0% {
    d: path("M0,20 Q10,5 20,20 T40,20 T60,20...");
  }
  25% {
    d: path("M0,20 Q10,35 20,20 T40,15 T60,25...");
  }
  50% {
    d: path("M0,20 Q10,10 20,20 T40,30 T60,10...");
  }
  75% {
    d: path("M0,20 Q10,30 20,20 T40,10 T60,30...");
  }
  100% {
    d: path("M0,20 Q10,5 20,20 T40,20 T60,20...");
  }
}

Note: Animating the d attribute works in modern browsers but check caniuse for support.

Adding Glitch Corruption Blocks

To make the oscilloscope feel more “broken”, I added random corruption blocks that flash in and out:

.sw-glitch-block {
  position: absolute;
  opacity: 0;
  mix-blend-mode: difference;
  animation: glitch-block 4s steps(1) infinite;
}

@keyframes glitch-block {
  0%, 92%, 100% {
    opacity: 0;
    transform: translateX(0);
  }
  93% {
    opacity: 0.8;
    transform: translateX(-5px);
    background: var(--glitch-cyan);
  }
  95% {
    opacity: 0.6;
    transform: translateX(3px);
    background: var(--glitch-red);
  }
  97% {
    opacity: 0.9;
    transform: translateX(-2px);
    background: var(--color-success);
  }
}

The steps(1) timing function is crucial - it makes the transitions instant rather than smooth, which feels more like digital corruption.

Text Glitch Animations

For text, I used a combination of text-shadow for chromatic aberration and transform for position jitter:

.glitch-text {
  animation: text-glitch 6s ease-in-out infinite;
}

@keyframes text-glitch {
  0%, 92%, 100% {
    transform: translateX(0);
    text-shadow: none;
  }
  93% {
    transform: translateX(-2px);
    text-shadow:
      2px 0 var(--glitch-cyan),
      -2px 0 var(--glitch-red);
  }
  94% {
    transform: translateX(2px);
    text-shadow:
      -2px 0 var(--glitch-cyan),
      2px 0 var(--glitch-red);
  }
  95% {
    transform: translateX(-1px);
    text-shadow:
      1px 0 var(--glitch-cyan),
      -1px 0 var(--glitch-red);
  }
  96% {
    transform: translateX(1px);
    text-shadow: none;
  }
}

The effect triggers rarely (only 8% of the animation cycle) which keeps it feeling like an occasional glitch rather than constant noise.

Respecting User Preferences

This is really important - some users have vestibular disorders or simply find animations distracting. Always respect prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  .crt-filter::before,
  .crt-filter::after {
    animation: none;
  }

  .glitch-text {
    animation: none;
  }

  img.crt-image {
    animation: none;
  }
}

This disables all the glitch animations for users who have requested reduced motion, while keeping the static styling intact.

Putting It All Together

Here’s an example of how I combined these effects for my Now Playing widget:

.now-playing-card {
  background: var(--theme-bg-secondary);
  border: 2px solid rgba(255, 255, 255, 0.08);
}

.now-playing-card:hover {
  border-color: var(--color-success);
  transform: translate(-2px, -2px);
  box-shadow: 2px 2px 0 var(--color-success);
}

.now-playing-artwork img {
  filter: grayscale(100%);
  transition: filter 0.3s;
}

.now-playing-card:hover .now-playing-artwork img {
  filter: grayscale(0%);
  animation: art-glitch 3s steps(1) infinite;
}

The key to making glitch effects feel refined:

  1. Restraint - Don’t glitch everything constantly. Use hover states and timing.
  2. Consistency - Use the same colour palette and timing functions throughout.
  3. Purpose - Each effect should enhance the aesthetic, not distract from content.
  4. Accessibility - Always provide reduced-motion alternatives.

Live Examples

Here are some examples you can see in action on this very site:

EffectWhere to see it
CRT Scan LinesHover over any album art on the Playlists page
Chromatic AberrationAll images site-wide have subtle red/cyan edges
Text GlitchWatch the page titles - they glitch every few seconds
OscilloscopeKeep an eye out for me playing some music on spotify (oh I’m not playing anything I guess you’ll have to keep coming back 😅 )
Corruption BlocksThe Now Playing oscilloscope has random glitch blocks

Quick Reference: CSS Properties Used

Here’s a cheat sheet of the key CSS properties that make these effects work:

PropertyUse Case
repeating-linear-gradientCRT scan lines
box-shadow with rgba coloursChromatic aberration
text-shadowText chromatic aberration
mix-blend-mode: differenceGlitch block overlays
animation: steps(1)Instant/digital transitions
filter: hue-rotate()Colour shifting in glitches
transform: translateX()Position jitter
SVG d path animationMorphing waveforms

Wrapping Up

Building these glitch effects was a fun exercise in pure CSS animation. The Neo-Brutalist aesthetic gives you permission to be bold and experimental, but the key is keeping things intentional. Every scan line, every chromatic shift, every corruption block should feel like a design choice, not an accident.

If you want to see these effects in action, have a browse around this site - they’re everywhere from the page titles to the Spotify widget to the image galleries.

Resources

Got questions or want to show me what you’ve built? Hit me up on the socials - I’d love to see where people take this!

Until next time, av’a good’en! 👋