CSS animations let you animate transitions between multiple states using @keyframes rules, without JavaScript. Unlike CSS transitions (which animate between two states triggered by a state change like :hover), CSS animations run continuously or a set number of times, can animate through as many intermediate states as you define, and can start automatically on page load. The two components are: the @keyframes rule (which defines the animation frames using percentages from 0% to 100%, or from/to shorthand), and the animation property on the element (which references the keyframe name and sets duration, timing function, delay, iteration count, and direction). The animation-fill-mode property controls the element’s state before and after the animation runs: forwards holds the final keyframe state after the animation ends (essential for entrance animations), backwards applies the first keyframe during the delay period, and both does both. Performance-wise, only animate transform and opacity for GPU-accelerated animations that do not trigger layout or paint — animating width, height, top, or left causes expensive layout recalculations on every frame. Use will-change: transform on elements you plan to animate to hint the browser to promote them to their own compositor layer before the animation starts. Combined with position: sticky (from the sticky guide) and Intersection Observer, CSS keyframe animations are the foundation of modern WordPress theme motion design.
Problem: You want to add entrance animations, loading spinners, and hover effects to your WordPress theme without JavaScript or animation libraries.
Solution: Add the following CSS patterns to your theme stylesheet:
/* ── Fade-in entrance animation ────────────────────────────────────────────── */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp 0.5s ease both; /* 'both' = apply first frame during delay too */
}
/* Staggered list items */
.card-list .card:nth-child(1) { animation: fadeInUp 0.5s ease 0.0s both; }
.card-list .card:nth-child(2) { animation: fadeInUp 0.5s ease 0.1s both; }
.card-list .card:nth-child(3) { animation: fadeInUp 0.5s ease 0.2s both; }
/* ── Infinite loading spinner ──────────────────────────────────────────────── */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(0,0,0,.15);
border-top-color: #0073aa;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
/* ── Pulse effect (e.g. "live" badge) ──────────────────────────────────────── */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.7; }
}
.live-badge {
animation: pulse 1.8s ease-in-out infinite;
}
/* ── Skeleton loading shimmer ──────────────────────────────────────────────── */
@keyframes shimmer {
from { background-position: -400px 0; }
to { background-position: 400px 0; }
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 800px 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
/* ── Pause animations for users who prefer reduced motion ──────────────────── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
NOTE: Always include the @media (prefers-reduced-motion: reduce) block — users with vestibular disorders can set their OS to prefer reduced motion, and ignoring this causes real accessibility issues. The WordPress accessibility guidelines (WCAG 2.1, Success Criterion 2.3.3) require respecting this preference. The animation-play-state: paused property lets you pause and resume an animation with JavaScript by toggling a CSS class, which is more efficient than removing and re-adding the animation property (re-adding restarts from frame 0).