CSS Animations: Creating Smooth Keyframe Animations
Movies aren't static images but flowing visuals formed by playing 24 frames of images per second. CSS animations use the same principle—by defining keyframes, elements can smoothly transition between multiple states. Unlike transitions, which can only change between two states when triggered, animations can create complex multi-step animations that play automatically without user interaction, making them core tools for loading indicators, decorative effects, and entrance animations.
Animation vs Transition: When to Use Which?
Before learning animations, understand their differences from transitions:
| Feature | Transition | Animation |
|---|---|---|
| Trigger | Requires state change (e.g., hover) | Plays automatically, no trigger needed |
| Control | Only start and end points | Multiple keyframes can be defined |
| Looping | Doesn't support | Supports infinite looping |
| Direction | Only forward or reverse | Supports alternate, reverse, etc. |
| Pause/Resume | Not supported | Supported |
| Use Cases | Interactive feedback (buttons, links) | Continuous animations (loaders, decorations) |
Simple judgment:
- User-triggered simple effects → Transition
- Auto-play or complex multi-step → Animation
@keyframes: Defining Animation Sequences
@keyframes rules define keyframes of an animation on the timeline, with each keyframe describing the element's style at that moment.
Basic Syntax
/* Define animation named slidein */
@keyframes slidein {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Use percentages for more precise timing points */
@keyframes fadeInOut {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}- from equals 0% (animation start)
- to equals 100% (animation end)
- Can define any percentage keyframe (e.g., 25%, 33.33%, 75%)
Multi-Property Keyframes
Each keyframe can contain multiple CSS properties.
@keyframes complexAnimation {
0% {
transform: translateX(0) rotate(0deg) scale(1);
background-color: #3498db;
opacity: 0.5;
}
50% {
transform: translateX(100px) rotate(180deg) scale(1.5);
background-color: #e74c3c;
opacity: 1;
}
100% {
transform: translateX(0) rotate(360deg) scale(1);
background-color: #2ecc71;
opacity: 0.5;
}
}Same Style at Multiple Keyframes
Multiple time points can share styles, separated by commas.
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}Animation Properties: Controlling Animation Playback
After defining keyframes, use animation properties to apply them to elements.
animation-name: Animation Name
Specify which @keyframes to use.
.box {
animation-name: slidein;
}animation-duration: Animation Length
Time for one complete animation cycle.
.box {
animation-duration: 2s; /* 2 seconds */
}
.fast-animation {
animation-duration: 500ms; /* 0.5 seconds */
}animation-timing-function: Easing Function
Controls the animation's speed curve, same as transition-timing-function.
.linear {
animation-timing-function: linear; /* Uniform speed */
}
.ease {
animation-timing-function: ease; /* Slow-fast-slow (default) */
}
.ease-in-out {
animation-timing-function: ease-in-out; /* Slow start and end */
}
.custom {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Bouncing effect */
}Note: Easing functions apply between every two keyframes, not the entire animation.
animation-delay: Start Delay
Wait time before animation starts.
.delayed {
animation-delay: 1s; /* Start 1 second later */
}
.negative-delay {
animation-delay: -1s; /* Negative: skip first 1s, start from middle */
}animation-iteration-count: Play Count
Number of times animation repeats.
.play-once {
animation-iteration-count: 1; /* Play 1 time (default) */
}
.play-three-times {
animation-iteration-count: 3; /* Play 3 times */
}
.loop-forever {
animation-iteration-count: infinite; /* Infinite loop */
}
.fractional {
animation-iteration-count: 2.5; /* Play 2.5 times */
}animation-direction: Play Direction
Controls whether animation plays forward, backward, or alternates.
.normal {
animation-direction: normal;
/* Always 0% → 100% (default) */
}
.reverse {
animation-direction: reverse;
/* Always 100% → 0% */
}
.alternate {
animation-direction: alternate;
/* Odd iterations forward (0%→100%), even backward (100%→0%) */
}
.alternate-reverse {
animation-direction: alternate-reverse;
/* Odd iterations backward, even forward */
}Practical use: alternate with infinite can create back-and-forth swinging effects.
animation-fill-mode: Fill Mode
Controls element's style before and after animation.
.none-fill {
animation-fill-mode: none;
/* Default: return to original style after animation ends */
}
.forwards {
animation-fill-mode: forwards;
/* Keep last frame style (100% or 0% depending on direction) */
}
.backwards {
animation-fill-mode: backwards;
/* Apply first frame style during delay */
}
.both {
animation-fill-mode: both;
/* Apply both forwards and backwards */
}Commonly used: forwards makes element maintain animation end state.
animation-play-state: Play/Pause
Controls whether animation is playing or paused.
.running {
animation-play-state: running; /* Playing (default) */
}
.paused {
animation-play-state: paused; /* Paused */
}
/* Practical application: pause on hover */
.animation-box {
animation: spin 2s linear infinite;
}
.animation-box:hover {
animation-play-state: paused;
}animation Shorthand Property
In development, shorthand form is more commonly used:
/* Syntax: name duration timing-function delay iteration-count direction fill-mode play-state */
.box {
animation: slidein 2s ease-in-out 0.5s infinite alternate both running;
}
/* Common simplified version */
.box {
animation: slidein 2s infinite;
}
/* Multiple animations, comma-separated */
.box {
animation: slidein 2s, fadeIn 1s 0.5s, rotate 3s infinite;
}Order tip: First time value is duration, second is delay.
Practical Applications: Common Animation Effects
Loading Indicator: Rotating Circle
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}Pulse Effect
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.7;
}
}
.pulse-button {
animation: pulse 2s ease-in-out infinite;
}Bounce Animation
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.bounce-element {
animation: bounce 2s infinite;
}Fade In and Out
@keyframes fadeInOut {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
.fade-text {
animation: fadeInOut 3s infinite;
}Slide In Effect
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideInLeft 0.5s ease-out forwards;
}Shake Effect
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-10px);
}
20%,
40%,
60%,
80% {
transform: translateX(10px);
}
}
.shake-on-error {
animation: shake 0.5s;
}Typewriter Effect
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink {
50% {
border-color: transparent;
}
}
.typewriter {
width: 0;
overflow: hidden;
white-space: nowrap;
border-right: 3px solid #333;
animation: typing 3.5s steps(40) 1s forwards, blink 0.75s step-end infinite;
}Practical Case Study: Complex Loading Animation
Create a multi-dot bouncing loading indicator.
<div class="loader">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.loader {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
height: 100px;
}
.dot {
width: 15px;
height: 15px;
background: #3498db;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
/* Stagger animation timing */
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
.dot:nth-child(3) {
animation-delay: 0s;
}Practical Case Study: Progress Bar Animation
A filling progress bar.
<div class="progress-container">
<div class="progress-bar"></div>
</div>@keyframes fillProgress {
from {
width: 0%;
}
to {
width: 100%;
}
}
.progress-container {
width: 100%;
height: 30px;
background: #ecf0f1;
border-radius: 15px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71);
border-radius: 15px;
animation: fillProgress 3s ease-out forwards;
}Practical Case Study: Floating Icon
Create an icon that floats up and down.
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
@keyframes floatShadow {
0%,
100% {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
50% {
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.1);
}
}
.floating-icon {
width: 60px;
height: 60px;
background: #3498db;
border-radius: 50%;
animation: float 3s ease-in-out infinite, floatShadow 3s ease-in-out infinite;
}Practical Case Study: Card Flip Animation
Auto-flipping card.
<div class="auto-flip-card">
<div class="card-inner">
<div class="card-front">Front</div>
<div class="card-back">Back</div>
</div>
</div>@keyframes flip {
0%,
45% {
transform: rotateY(0deg);
}
50%,
95% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(360deg);
}
}
.auto-flip-card {
width: 200px;
height: 300px;
perspective: 1000px;
}
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
animation: flip 6s infinite;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
border-radius: 12px;
}
.card-front {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.card-back {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
transform: rotateY(180deg);
}Combining Multiple Animations
An element can run multiple animations simultaneously.
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes colorChange {
0% {
background: #3498db;
}
50% {
background: #e74c3c;
}
100% {
background: #2ecc71;
}
}
@keyframes scale {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
.multi-animation {
animation: spin 3s linear infinite, colorChange 6s ease-in-out infinite,
scale 2s ease-in-out infinite;
}Note: If multiple animations modify the same property (like transform), later ones will override earlier ones. Need to combine them into one @keyframes.
JavaScript Animation Control
You can dynamically control animations with JavaScript.
const element = document.querySelector(".box");
// Add animation
element.style.animation = "slidein 2s ease-out forwards";
// Remove animation
element.style.animation = "none";
// Pause animation
element.style.animationPlayState = "paused";
// Resume animation
element.style.animationPlayState = "running";
// Listen for animation events
element.addEventListener("animationstart", () => {
console.log("Animation started");
});
element.addEventListener("animationiteration", () => {
console.log("Animation iteration");
});
element.addEventListener("animationend", () => {
console.log("Animation ended");
});Performance Optimization and Best Practices
Prioritize Transform and Opacity
/* ❌ Low performance: triggers reflow/repaint */
@keyframes badAnimation {
from {
width: 100px;
height: 100px;
left: 0;
}
to {
width: 200px;
height: 200px;
left: 100px;
}
}
/* ✅ High performance: only triggers compositing */
@keyframes goodAnimation {
from {
transform: scale(1) translateX(0);
opacity: 1;
}
to {
transform: scale(2) translateX(100px);
opacity: 0.5;
}
}Reduce Number of Animated Elements
/* ⚠️ Hundreds of elements animating simultaneously will cause lag */
.many-items {
animation: complex 2s infinite;
}
/* ✅ Only animate necessary elements */
.featured-item {
animation: highlight 2s infinite;
}Use will-change
.heavy-animation {
will-change: transform, opacity;
animation: complexMove 2s infinite;
}Control Frame Rate: Use steps()
For sprite animations, use steps() instead of smooth transitions.
@keyframes spriteAnimation {
to {
background-position: -480px 0; /* Assume 8 frames, 60px each */
}
}
.sprite {
width: 60px;
height: 60px;
background: url("sprite.png") no-repeat;
animation: spriteAnimation 0.8s steps(8) infinite;
/* 8 steps, each frame shows 0.1s */
}Common Issues and Solutions
Issue 1: Animation Plays Immediately on Page Load
Sometimes you want users to see a state before animation starts.
/* Use animation-delay */
.delayed-start {
animation: fadeIn 1s 2s forwards;
/* Delay 2s before starting */
}
/* Or use JavaScript control */setTimeout(() => {
document.querySelector(".box").classList.add("animated");
}, 2000);Issue 2: Flicker After Animation Ends
Use animation-fill-mode: forwards to maintain the last frame.
.fade-out {
animation: fadeOut 1s forwards; /* Won't jump back to initial state */
}Issue 3: Infinite Animations Consume Resources
Pause animations when not visible.
document.addEventListener("visibilitychange", () => {
const animations = document.querySelectorAll(".animated");
animations.forEach((el) => {
el.style.animationPlayState = document.hidden ? "paused" : "running";
});
});Issue 4: Animations Not Smooth on Mobile
/* Add hardware acceleration */
.smooth-animation {
transform: translate3d(0, 0, 0); /* Trigger GPU acceleration */
will-change: transform;
animation: move 2s infinite;
}Accessibility Considerations
Some users may be sensitive to animations, so respect system settings.
/* Check if user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Or only disable specific animations */
@media (prefers-reduced-motion: reduce) {
.decorative-animation {
animation: none;
}
.functional-animation {
animation-duration: 0.3s; /* Keep but shorten */
}
}Summary
CSS animation is a powerful tool for creating dynamic, eye-catching visual experiences:
Core Concepts:
- @keyframes: Define animation sequence keyframes
- animation-name: Specify animation to use
- animation-duration: Animation length
- animation-iteration-count: Play count (can be infinite)
- animation-direction: Play direction
- animation-fill-mode: Before and after animation states
Best Practices:
- Performance first: Use
transformandopacity - Moderate use: Not all elements need animation
- Consider accessibility: Respect
prefers-reduced-motion - Semantic naming: Animation names should describe their effect
- Test performance: Verify smoothness on low-end devices
When to Use Animation:
- Loading indicators (rotating, pulsing, progress bars)
- Decorative animations (background patterns, floating elements)
- Guide animations (arrow indicators, highlight tips)
- Brand animations (logo animations, signature effects)
CSS animations bring static pages to life, but remember—animations are tools for enhancing user experience, not the goal itself. The best animations are those that enhance understanding, provide feedback, and guide attention, not those that distract or show off. Elegant animations should feel as natural as breathing—users will perceive their presence without being disturbed by them.