Modern CSS Animations
Keyframe animations for cards, icons, loaders and headings - drop a class on an element and it comes alive.
1Bouncing Text
Makes each letter of a short word hop in turn; wrap every letter in a span with --i set to its index. Class: fx-bouncing-text
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3 p-3 h-100" style="min-height:200px">
<h2 class="display-5 fw-bold mb-0 fx-bouncing-text" aria-label="Bounce"><span style="--i:0" aria-hidden="true">B</span><span style="--i:1" aria-hidden="true">o</span><span style="--i:2" aria-hidden="true">u</span><span style="--i:3" aria-hidden="true">n</span><span style="--i:4" aria-hidden="true">c</span><span style="--i:5" aria-hidden="true">e</span></h2>
</div>
/* CSS (already in assets/css/effects.css) */
.fx-bouncing-text { display: inline-flex; }
.fx-bouncing-text > span {
display: inline-block;
animation: fx-bouncing-text-bounce 1.6s ease-in-out infinite;
animation-delay: calc(var(--i, 0) * .09s);
}
@keyframes fx-bouncing-text-bounce {
0%, 60%, 100% { transform: translateY(0); }
25% { transform: translateY(-14px); color: var(--wt-primary-ink); }
40% { transform: translateY(2px); }
}
@media (prefers-reduced-motion: reduce) {
.fx-bouncing-text, .fx-bouncing-text::before, .fx-bouncing-text::after, .fx-bouncing-text *, .fx-bouncing-text *::before, .fx-bouncing-text *::after { animation: none !important; transition: none !important; }
}2Glitch Text
Splits a heading into offset colour slices that jitter like a broken screen; repeat the text in data-text. Class: fx-glitch-text
SYSTEM ERROR
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3 p-3 h-100" style="min-height:200px">
<div class="bg-dark rounded-4 px-4 py-5 text-center w-100" style="max-width:19rem">
<h2 class="display-6 fw-bold text-white mb-0 fx-glitch-text" data-text="SYSTEM ERROR">SYSTEM ERROR</h2>
</div>
</div>
/* CSS (already in assets/css/effects.css) */
.fx-glitch-text { position: relative; display: inline-block; }
.fx-glitch-text::before, .fx-glitch-text::after {
content: attr(data-text); position: absolute; inset: 0; pointer-events: none;
-webkit-clip-path: inset(50% 0 50% 0); clip-path: inset(50% 0 50% 0);
}
.fx-glitch-text::before { color: var(--wt-accent); left: 2px; text-shadow: -1px 0 var(--wt-primary); animation: fx-glitch-text-a 2.6s steps(1) infinite; }
.fx-glitch-text::after { color: var(--wt-secondary); left: -2px; text-shadow: 1px 0 var(--wt-accent); animation: fx-glitch-text-b 3.1s steps(1) infinite; }
@keyframes fx-glitch-text-a {
0% { -webkit-clip-path: inset(0 0 82% 0); clip-path: inset(0 0 82% 0); transform: translate(-3px, -1px); }
15% { -webkit-clip-path: inset(30% 0 52% 0); clip-path: inset(30% 0 52% 0); transform: translate(3px, 1px); }
30% { -webkit-clip-path: inset(62% 0 20% 0); clip-path: inset(62% 0 20% 0); transform: translate(-2px, 0); }
45% { -webkit-clip-path: inset(50% 0 50% 0); clip-path: inset(50% 0 50% 0); }
80% { -webkit-clip-path: inset(12% 0 70% 0); clip-path: inset(12% 0 70% 0); transform: translate(2px, -1px); }
90% { -webkit-clip-path: inset(50% 0 50% 0); clip-path: inset(50% 0 50% 0); }
}
@keyframes fx-glitch-text-b {
0% { -webkit-clip-path: inset(70% 0 8% 0); clip-path: inset(70% 0 8% 0); transform: translate(2px, 1px); }
20% { -webkit-clip-path: inset(8% 0 76% 0); clip-path: inset(8% 0 76% 0); transform: translate(-3px, 0); }
35% { -webkit-clip-path: inset(50% 0 50% 0); clip-path: inset(50% 0 50% 0); }
70% { -webkit-clip-path: inset(40% 0 38% 0); clip-path: inset(40% 0 38% 0); transform: translate(3px, -1px); }
85% { -webkit-clip-path: inset(50% 0 50% 0); clip-path: inset(50% 0 50% 0); }
}
@media (prefers-reduced-motion: reduce) {
.fx-glitch-text, .fx-glitch-text::before, .fx-glitch-text::after, .fx-glitch-text *, .fx-glitch-text *::before, .fx-glitch-text *::after { animation: none !important; transition: none !important; }
}