How the Glitch Effect Works




Edit Me Too

I’m kinda proud of the effect. It’s pretty dumb, and could definitely induce motion sickness if you stare at it for too long. But…​ It looks cool.

Also, after far, far too long I am (just about) ready to say this site is at version 1.0. Hurrah.


HTML

There are 2 things to see here. Firstly, I set the content attribute to be equal to the title. Secondly and unrelatedly, I vary the font size to make the title take up a reasonable amount of the page.

 1<h1 class="glitch" content="{{ title .Title }}"
 2style="
 3	{{ if lt (len .Title) 20 }}
 4		font-size:15vmin
 5	{{ else if gt (len .Title) 60 }}
 6		font-size:8vmin
 7	{{ else }}
 8		font-size:{{ len .Title | math.Log | mul -7 | add 37 | math.Floor  }}vmin
 9	{{end}}"
10>{{ title .Title }}</h1>

CSS

This is largely unremarkable, just a load of animation keyframes.

Do pay attention to the content and animation delay attributes though. content: attr(content) is the trick that allows me to easily set the overlay text to match. This is important because I can easily set html content on a per page basis with Hugo (the static site generator I use), but it’s substantially harder to have unique CSS per page. animation-delay isn’t useful for the majority of titles, but for the homepage I have each letter as its own separate animation. I’ve manually set a unique animation delay on each and that ensures the wandering clip paths don’t all line up in an obvious way.

  1@media (prefers-reduced-motion: no-preference) {
  2	.glitch {
  3		font-weight: 700;
  4		position: relative;
  5		animation: glitch-text 1199ms infinite -200ms;
  6	}
  7
  8	.glitch::before,
  9	.glitch::after {
 10		position: absolute;
 11		left: 0;
 12		top: 0;
 13		opacity: 0.8;
 14		content: attr(content);
 15		animation-delay: inherit;
 16	}
 17
 18	.glitch::before {
 19		animation: glitch-text 650ms infinite alternate,
 20			outer 1000ms infinite,
 21			before 1538ms linear -1500ms infinite alternate-reverse;
 22		clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
 23	}
 24
 25	.glitch::after {
 26		animation: glitch-text 375ms reverse infinite, outer 957ms -500ms infinite reverse;
 27		clip-path: polygon(0 80%, 100% 20%, 100% 100%, 0 100%);
 28	}
 29
 30	@keyframes glitch-text {
 31		0% {
 32			text-shadow: 0.05em 0 12px rgba(255, 0, 0, 0.75),
 33				-0.05em -0.025em 10px rgba(0, 255, 0, 0.75),
 34				-0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
 35		}
 36
 37		14% {
 38			text-shadow: 0.05em 0 0 rgba(255, 0, 0, 0.75),
 39				-0.05em -0.025em 0 rgba(0, 255, 0, 0.75),
 40				-0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
 41		}
 42
 43		15% {
 44			text-shadow: -0.05em -0.025em 0 rgba(255, 0, 0, 0.75),
 45				0.025em 0.025em 5px rgba(0, 255, 0, 0.75),
 46				-0.05em -0.05em 3px rgba(0, 0, 255, 0.75);
 47		}
 48
 49		49% {
 50			text-shadow: -0.05em -0.025em 0 rgba(255, 0, 0, 0.75),
 51				0.025em 0.025em 0 rgba(0, 255, 0, 0.75),
 52				-0.05em -0.05em 0 rgba(0, 0, 255, 0.75);
 53		}
 54
 55		50% {
 56			text-shadow: 0.025em 0.05em 0 rgba(255, 0, 0, 0.75),
 57				0.05em 0 0 rgba(0, 255, 0, 0.75),
 58				0 -0.05em 0 rgba(0, 0, 255, 0.75);
 59		}
 60
 61		99% {
 62			text-shadow: 0.025em 0.05em 0 rgba(255, 0, 0, 0.75),
 63				0.05em 0 0 rgba(0, 255, 0, 0.75),
 64				0 -0.05em 0 rgba(0, 0, 255, 0.75);
 65		}
 66
 67		100% {
 68			text-shadow: -0.025em 0 5px rgba(255, 0, 0, 0.75),
 69				-0.025em -0.025em 0 rgba(0, 255, 0, 0.75),
 70				-0.025em -0.05em 12px rgba(0, 0, 255, 0.75);
 71		}
 72	}
 73
 74	@keyframes outer {
 75		0% {
 76			opacity: 30%;
 77		}
 78
 79		14% {
 80			opacity: 20%;
 81			transform: translate(-0.04em, -0.026em);
 82		}
 83
 84		15% {
 85			opacity: 100%;
 86			transform: translate(0.02em, 0.016em) rotate(-0.5deg);
 87		}
 88
 89		49% {
 90			transform: rotate(0.1deg);
 91			opacity: 80%;
 92		}
 93
 94		50% {
 95			transform: rotate(-0.2deg);
 96		}
 97
 98		60% {
 99			transform: rotate(0.1deg) translate(0.015em, 0.002em);
100			opacity: 40%;
101		}
102	}
103
104	@keyframes before {
105		0% {
106			clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
107		}
108
109		20% {
110			clip-path: polygon(0 0, 100% 0, 100% 45%, 0 75%);
111		}
112
113		22% {
114			clip-path: polygon(0 0, 100% 0, 100% 35%, 0 25%);
115		}
116
117		75% {
118			clip-path: polygon(0 0, 100% 0, 100% 35%, 0 25%);
119		}
120
121		76% {
122			clip-path: polygon(0 0, 100% 0, 100% 5%, 0 75%);
123		}
124	}
125}