CSS animations create engaging experiences with keyframes and animation properties. Here's how to master them.
Basic Keyframes#
1/* Define animation with @keyframes */
2@keyframes fadeIn {
3 from {
4 opacity: 0;
5 }
6 to {
7 opacity: 1;
8 }
9}
10
11/* Apply animation */
12.fade-in {
13 animation: fadeIn 1s ease-in-out;
14}
15
16/* Percentage keyframes */
17@keyframes bounce {
18 0% {
19 transform: translateY(0);
20 }
21 50% {
22 transform: translateY(-20px);
23 }
24 100% {
25 transform: translateY(0);
26 }
27}
28
29/* Multiple keyframes */
30@keyframes pulse {
31 0%, 100% {
32 transform: scale(1);
33 }
34 50% {
35 transform: scale(1.1);
36 }
37}Animation Properties#
1/* Individual properties */
2.animated {
3 animation-name: slideIn;
4 animation-duration: 1s;
5 animation-timing-function: ease-out;
6 animation-delay: 0.5s;
7 animation-iteration-count: 3;
8 animation-direction: alternate;
9 animation-fill-mode: forwards;
10 animation-play-state: running;
11}
12
13/* Shorthand */
14.animated {
15 animation: slideIn 1s ease-out 0.5s 3 alternate forwards;
16 /* name | duration | timing | delay | iterations | direction | fill-mode */
17}
18
19/* Multiple animations */
20.multi {
21 animation:
22 fadeIn 1s ease-out,
23 slideUp 0.5s ease-in-out,
24 bounce 2s infinite;
25}Timing Functions#
1/* Built-in timing functions */
2.timing {
3 animation-timing-function: linear;
4 animation-timing-function: ease;
5 animation-timing-function: ease-in;
6 animation-timing-function: ease-out;
7 animation-timing-function: ease-in-out;
8}
9
10/* Steps for frame-by-frame */
11.steps {
12 animation-timing-function: steps(4, start);
13 animation-timing-function: steps(4, end);
14 animation-timing-function: step-start;
15 animation-timing-function: step-end;
16}
17
18/* Custom cubic-bezier */
19.custom {
20 animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
21}
22
23/* Common cubic-bezier presets */
24.ease-out-back {
25 animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
26}
27
28.ease-in-out-back {
29 animation-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6);
30}Iteration and Direction#
1/* Iteration count */
2.iterate {
3 animation-iteration-count: 1; /* Play once */
4 animation-iteration-count: 3; /* Play 3 times */
5 animation-iteration-count: infinite; /* Loop forever */
6 animation-iteration-count: 2.5; /* Play 2.5 times */
7}
8
9/* Direction */
10.direction {
11 animation-direction: normal; /* 0% -> 100% */
12 animation-direction: reverse; /* 100% -> 0% */
13 animation-direction: alternate; /* 0% -> 100% -> 0% -> ... */
14 animation-direction: alternate-reverse; /* 100% -> 0% -> 100% -> ... */
15}
16
17/* Infinite bounce */
18.bounce {
19 animation: bounce 0.5s ease-in-out infinite alternate;
20}Fill Mode#
1/* Fill mode controls state before/after animation */
2.fill {
3 animation-fill-mode: none; /* Default - no styles applied */
4 animation-fill-mode: forwards; /* Keep end state */
5 animation-fill-mode: backwards; /* Apply start state during delay */
6 animation-fill-mode: both; /* Both forwards and backwards */
7}
8
9/* Example: Fade in and stay visible */
10@keyframes fadeIn {
11 from { opacity: 0; }
12 to { opacity: 1; }
13}
14
15.fade-in {
16 opacity: 0; /* Initial state */
17 animation: fadeIn 1s ease-out forwards;
18 /* forwards keeps opacity: 1 after animation */
19}
20
21/* Apply start state during delay */
22.delayed {
23 animation: fadeIn 1s ease-out 2s backwards;
24 /* opacity: 0 applied during 2s delay */
25}Play State#
1/* Control animation playback */
2.controllable {
3 animation: spin 2s linear infinite;
4 animation-play-state: running;
5}
6
7.controllable:hover {
8 animation-play-state: paused;
9}
10
11/* Pause on hover */
12.pause-on-hover {
13 animation: rotate 3s linear infinite;
14}
15
16.pause-on-hover:hover {
17 animation-play-state: paused;
18}Common Animations#
1/* Fade In */
2@keyframes fadeIn {
3 from { opacity: 0; }
4 to { opacity: 1; }
5}
6
7/* Slide In */
8@keyframes slideInLeft {
9 from {
10 transform: translateX(-100%);
11 opacity: 0;
12 }
13 to {
14 transform: translateX(0);
15 opacity: 1;
16 }
17}
18
19/* Scale */
20@keyframes scaleUp {
21 from { transform: scale(0); }
22 to { transform: scale(1); }
23}
24
25/* Rotate */
26@keyframes spin {
27 from { transform: rotate(0deg); }
28 to { transform: rotate(360deg); }
29}
30
31/* Shake */
32@keyframes shake {
33 0%, 100% { transform: translateX(0); }
34 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
35 20%, 40%, 60%, 80% { transform: translateX(5px); }
36}
37
38/* Pulse */
39@keyframes pulse {
40 0% { transform: scale(1); }
41 50% { transform: scale(1.05); }
42 100% { transform: scale(1); }
43}
44
45/* Bounce */
46@keyframes bounce {
47 0%, 20%, 50%, 80%, 100% {
48 transform: translateY(0);
49 }
50 40% {
51 transform: translateY(-30px);
52 }
53 60% {
54 transform: translateY(-15px);
55 }
56}Loading Spinners#
1/* Simple spinner */
2@keyframes spin {
3 to { transform: rotate(360deg); }
4}
5
6.spinner {
7 width: 40px;
8 height: 40px;
9 border: 4px solid #f3f3f3;
10 border-top: 4px solid #3498db;
11 border-radius: 50%;
12 animation: spin 1s linear infinite;
13}
14
15/* Dots loading */
16@keyframes dotPulse {
17 0%, 80%, 100% {
18 transform: scale(0);
19 opacity: 0;
20 }
21 40% {
22 transform: scale(1);
23 opacity: 1;
24 }
25}
26
27.dots {
28 display: flex;
29 gap: 8px;
30}
31
32.dot {
33 width: 12px;
34 height: 12px;
35 background: #3498db;
36 border-radius: 50%;
37 animation: dotPulse 1.4s ease-in-out infinite;
38}
39
40.dot:nth-child(1) { animation-delay: -0.32s; }
41.dot:nth-child(2) { animation-delay: -0.16s; }
42.dot:nth-child(3) { animation-delay: 0s; }
43
44/* Progress bar */
45@keyframes progress {
46 0% {
47 width: 0;
48 background-position: 0 0;
49 }
50 100% {
51 width: 100%;
52 background-position: 50px 0;
53 }
54}
55
56.progress-bar {
57 height: 4px;
58 background: linear-gradient(
59 45deg,
60 #3498db 25%,
61 #2980b9 25%,
62 #2980b9 50%,
63 #3498db 50%,
64 #3498db 75%,
65 #2980b9 75%
66 );
67 background-size: 50px 50px;
68 animation: progress 2s linear infinite;
69}Entrance Animations#
1/* Staggered entrance */
2.stagger > * {
3 opacity: 0;
4 animation: fadeInUp 0.5s ease-out forwards;
5}
6
7.stagger > *:nth-child(1) { animation-delay: 0.1s; }
8.stagger > *:nth-child(2) { animation-delay: 0.2s; }
9.stagger > *:nth-child(3) { animation-delay: 0.3s; }
10.stagger > *:nth-child(4) { animation-delay: 0.4s; }
11
12@keyframes fadeInUp {
13 from {
14 opacity: 0;
15 transform: translateY(20px);
16 }
17 to {
18 opacity: 1;
19 transform: translateY(0);
20 }
21}
22
23/* Using CSS custom properties */
24.stagger-item {
25 animation: fadeInUp 0.5s ease-out forwards;
26 animation-delay: calc(var(--i, 0) * 0.1s);
27}
28
29/* In HTML: style="--i: 0", style="--i: 1", etc. */Attention Seekers#
1/* Wiggle */
2@keyframes wiggle {
3 0%, 100% { transform: rotate(0); }
4 25% { transform: rotate(-5deg); }
5 75% { transform: rotate(5deg); }
6}
7
8.wiggle {
9 animation: wiggle 0.3s ease-in-out 3;
10}
11
12/* Heartbeat */
13@keyframes heartbeat {
14 0% { transform: scale(1); }
15 14% { transform: scale(1.3); }
16 28% { transform: scale(1); }
17 42% { transform: scale(1.3); }
18 70% { transform: scale(1); }
19}
20
21.heartbeat {
22 animation: heartbeat 1.5s ease-in-out infinite;
23}
24
25/* Flash */
26@keyframes flash {
27 0%, 50%, 100% { opacity: 1; }
28 25%, 75% { opacity: 0; }
29}
30
31.flash {
32 animation: flash 1s ease-in-out infinite;
33}Performance#
1/* GPU-accelerated properties */
2.performant {
3 /* Transform and opacity are GPU-accelerated */
4 transform: translateX(0);
5 opacity: 1;
6
7 /* Hint to browser */
8 will-change: transform, opacity;
9}
10
11/* Avoid animating these */
12.avoid {
13 /* Layout-triggering (expensive) */
14 animation: badAnimation 1s;
15}
16
17@keyframes badAnimation {
18 /* DON'T animate these */
19 from {
20 width: 100px;
21 height: 100px;
22 margin: 0;
23 padding: 0;
24 top: 0;
25 left: 0;
26 }
27 to {
28 width: 200px;
29 height: 200px;
30 margin: 20px;
31 padding: 20px;
32 top: 100px;
33 left: 100px;
34 }
35}
36
37/* DO this instead */
38@keyframes goodAnimation {
39 from {
40 transform: translate(0, 0) scale(1);
41 }
42 to {
43 transform: translate(100px, 100px) scale(2);
44 }
45}Reduced Motion#
1/* Respect user preference */
2@media (prefers-reduced-motion: reduce) {
3 *,
4 *::before,
5 *::after {
6 animation-duration: 0.01ms !important;
7 animation-iteration-count: 1 !important;
8 transition-duration: 0.01ms !important;
9 }
10}
11
12/* Alternative subtle animation */
13@media (prefers-reduced-motion: reduce) {
14 .animated {
15 animation: none;
16 opacity: 1; /* Show final state */
17 }
18}
19
20/* Provide motion-safe alternative */
21.element {
22 /* Reduced motion version */
23 opacity: 1;
24 transform: none;
25}
26
27@media (prefers-reduced-motion: no-preference) {
28 .element {
29 animation: fadeIn 0.5s ease-out;
30 }
31}Best Practices#
Performance:
✓ Animate transform and opacity
✓ Use will-change sparingly
✓ Keep animations short (< 500ms for UI)
✓ Test on low-end devices
Accessibility:
✓ Respect prefers-reduced-motion
✓ Avoid flashing content
✓ Don't animate essential content
✓ Provide static alternatives
UX:
✓ Use animation purposefully
✓ Keep animations subtle
✓ Match animation to content
✓ Consider mobile users
Avoid:
✗ Animating layout properties
✗ Too many simultaneous animations
✗ Infinite animations without purpose
✗ Blocking user interaction
Conclusion#
CSS animations bring interfaces to life with keyframes and animation properties. Use GPU-accelerated properties (transform, opacity) for smooth performance. Respect user preferences with prefers-reduced-motion. Keep animations purposeful and subtle - they should enhance, not distract from, the user experience. Master timing functions for natural-feeling motion and use fill-mode to control animation state.