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.

“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?”

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:
- CRT scan lines and the oscilloscope effect
- Chromatic aberration on images
- Text glitch animations
- Combining everything into a cohesive system
Let’s get into it!

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-gradientcreates the horizontal lines at 2px intervalssteps(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:
- Restraint - Don’t glitch everything constantly. Use hover states and timing.
- Consistency - Use the same colour palette and timing functions throughout.
- Purpose - Each effect should enhance the aesthetic, not distract from content.
- Accessibility - Always provide reduced-motion alternatives.
Live Examples
Here are some examples you can see in action on this very site:
| Effect | Where to see it |
|---|---|
| CRT Scan Lines | Hover over any album art on the Playlists page |
| Chromatic Aberration | All images site-wide have subtle red/cyan edges |
| Text Glitch | Watch the page titles - they glitch every few seconds |
| Oscilloscope | Keep 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 Blocks | The 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:
| Property | Use Case |
|---|---|
repeating-linear-gradient | CRT scan lines |
box-shadow with rgba colours | Chromatic aberration |
text-shadow | Text chromatic aberration |
mix-blend-mode: difference | Glitch block overlays |
animation: steps(1) | Instant/digital transitions |
filter: hue-rotate() | Colour shifting in glitches |
transform: translateX() | Position jitter |
SVG d path animation | Morphing 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
- MDN: CSS Animations
- CSS Tricks: A Complete Guide to CSS Gradients
- Can I Use: SVG d attribute animation
- Prefers Reduced Motion
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! 👋