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. It works through a combination of relatively simple animations layered together. If there’s no animation on the title, you may have reduced-motion set, or animations disabled on your device. Amusingly, I turn off animations on my phone so I can’t see the effect.
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.
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}