CSS Animation 101
Everything moves, you decide how. Pure CSS, no JavaScript, a real wow effect.
Recommended after CSS 101.
Chapter 01
Transitions — smooth changes
transition makes the change between two states smooth.
.btn, normal.btn:hoverThe order in the rule: property, duration, curve, delay.
transition: background 0.3s ease:hover
The easiest way into CSS motion: transition makes the change between two states smooth — a soft glide instead of an abrupt cut.
/* Without transition: abrupt jump */
.btn {
background: #333;
}
.btn:hover {
background: #fa5c7b;
}
/* With transition: smooth change */
.btn {
background: #333;
transition: background 0.3s ease;
/* ^property ^duration ^curve */
}
.btn:hover {
background: #fa5c7b;
}// Syntax
transition: [property] [duration] [timing-function] [delay]
transition: all 0.3s ease; — animates ALL changeable properties
transition: color 0.2s, transform 0.4s ease-out; — several, separated by commas
What can be animated with a transition?
Almost anything numeric: color, background, opacity, transform, width, height, padding, border-radius, box-shadow…
⚠ Performance
Where possible, animate only transform and opacity — these run on the GPU and are silky smooth. width, height, top, left trigger a layout recalculation = stutter.
Chapter 02
@keyframes — defining animations
First use @keyframes to define what happens. Then assign it to an element with animation.
@keyframes myFadewhat happens at 0 %, 50 %, 100 %.elementanimation: myFade 0.6s ease forwards@keyframes0% 50% 100%animation
With @keyframes you define an animation choreography — you decide what happens at 0%, 50% and 100%. CSS interpolates the rest automatically.
/* 1. Define the animation */
@keyframes myFade {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
/* 2. Apply the animation */
.element {
animation: myFade 0.6s ease forwards;
/* ^name ^duration ^curve ^keep end state */
}Live demos: the basic moves
/* Fade */
@keyframes fade { 0%,100% { opacity:1; } 50% { opacity:0.1; } }
/* Slide */
@keyframes slide { 0%,100% { transform: translateX(-30px); } 50% { transform: translateX(30px); } }
/* Spin */
@keyframes spin { to { transform: rotate(360deg); } }
/* Bounce */
@keyframes bounce {
0%,100% { transform: translateY(0); }
40% { transform: translateY(-40px); }
60% { transform: translateY(-20px); }
}
/* Scale + Rotate */
@keyframes scaleRotate {
0%,100% { transform: scale(1); }
50% { transform: scale(1.6) rotate(45deg); }
}Chapter 03
Animation properties at a glance
Each animation-… property controls one part. The shorthand combines them.
namewhich keyframesdurationone runtiming-functioncurvedelaydelayiteration-countinfinitedirectionalternatefill-modeforwardsplay-statepausedanimation: name duration timing delay iteration direction fill-mode
| Property | What it does | Example value |
|---|---|---|
| animation-name | Which @keyframes to use | fadeIn |
| animation-duration | How long one cycle lasts | 0.6s / 400ms |
| animation-timing-function | Speed curve | ease / linear |
| animation-delay | Delay before starting | 0.2s |
| animation-iteration-count | How often to repeat | infinite / 3 |
| animation-direction | Direction of the repetition | normal / alternate |
| animation-fill-mode | State before/after the animation | forwards / both |
| animation-play-state | Pause/play via CSS | paused / running |
// Shorthand
animation: name duration timing delay iteration direction fill-mode;
Example: animation: fadeIn 0.5s ease 0.2s 1 normal forwards;
fill-mode — often forgotten, always important
/* forwards = element stays in the end state of the animation */
.card-fade-in {
opacity: 0;
animation: fadeInUp 0.6s ease forwards;
}
/* Without forwards: the element jumps back to opacity:0 at the end! */Chapter 04
Timing functions — the soul of animation
The timing function decides how a movement feels.
linearmechanical: loading bar, spinnerease-outarriving: brakes gentlyease-inleaving: speeds upsteps(n)jumps: pixel art, typewriterease ease-in ease-out ease-in-outcubic-bezier()steps()
The timing function decides how an object feels. Same distance, same time — but a completely different effect:
| Value | Character | Ideal for |
|---|---|---|
| linear | Constant speed, mechanical | Progress bars, spinners |
| ease | Starts slowly, speeds up, slows down | General use, default |
| ease-in | Starts slowly, ends fast | Things that "leave" |
| ease-out | Starts fast, slows down gently | Things that "arrive" |
| ease-in-out | Like ease, but more symmetrical | Loops, hover effects |
| cubic-bezier(x,y,x,y) | Full control | Bounce, overshoot, custom |
| steps(n) | Jumps instead of transitions | Pixel art, typewriter |
/* Bounce effect via cubic-bezier */
animation: slideIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
/* ↑ value > 1 = Overshoot = springy feel */
/* Typewriter via steps */
animation: typing 3s steps(30) infinite;✅ Tip
cubic-bezier.com — a visual editor for your own curves. Just play around and copy the generated code!
Chapter 05
Transform — move, rotate, scale
transform changes how an element looks without shifting the layout.
translatemoverotaterotatescalegrow, shrinkOrder matters: translate rotate scale gives a different result from scale rotate translate.
translateX translateYrotate scale skewX
transform is the best friend of CSS animations — GPU-accelerated, no layout reflow, silky smooth.
/* Move */
transform: translateX(50px);
transform: translateY(-20px);
transform: translate(10px, -30px);
/* Scale */
transform: scale(1.5); /* 150% size */
transform: scaleX(2); /* horizontal only */
/* Rotate */
transform: rotate(45deg);
transform: rotate(-90deg);
/* Skew */
transform: skewX(15deg);
/* Combine — mind the order! */
transform: translateY(-10px) rotate(5deg) scale(1.05);⚠ Order matters with transform!
translate rotate scale ≠ scale rotate translate — the operations affect each other. Rule of thumb: translate first, then rotate, then scale.
transform-origin — changing the pivot point
/* Default: centre */
transform-origin: center;
/* Corner as pivot (like a calendar page) */
transform-origin: top left;
/* Exact position */
transform-origin: 20px 80%;Chapter 06
Useful animation patterns
Proven patterns: copy, adapt, combine.
@keyframes pulse-ringsteps()animation-delay
Pulse — eye-catching pulsing
/* PULSE RING */
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(250,92,123,0.5); }
70% { box-shadow: 0 0 0 20px rgba(250,92,123,0); }
100% { box-shadow: 0 0 0 0 rgba(250,92,123,0); }
}
.dot { animation: pulse-ring 1.5s ease-out infinite; }
/* TYPEWRITER */
@keyframes typing { 0%,5% { width:0 } 50%,95% { width: 200px } }
@keyframes blink { 50% { border-color: transparent; } }
.text {
overflow: hidden; white-space: nowrap;
width: 0; border-right: 2px solid currentColor;
animation: typing 3s steps(20) infinite, blink .7s step-end infinite;
}
/* STAGGER — fade in children one after another */
.list-item { opacity: 0; animation: fadeIn 0.4s ease forwards; }
.list-item:nth-child(1) { animation-delay: 0.1s; }
.list-item:nth-child(2) { animation-delay: 0.2s; }
.list-item:nth-child(3) { animation-delay: 0.3s; }
/* ... or with CSS custom properties for n elements */Hover lift — the card rises on hover
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}Shimmer / skeleton loading
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(90deg,
#1f1f23 25%, #2e2e35 50%, #1f1f23 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 6px;
}Chapter 07
PROJECT: your animated profile card
Five effects in one file, CSS only, no JavaScript.
animated-card.htmlone fileanimated-card.html- Browser
You build a fully animated profile card — with CSS only, no JavaScript. Page-load animation, hover effects, a pulse badge, the lot.
- Card fades in on load (stagger)
- Avatar with a pulse-ring animation
- Hover lifts the card (lift)
- Button with a shine effect on hover
- Badge pulses continuously
Create a file animated-card.html and paste in the complete code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Animated Card</title>
<style>
/* ── RESET & BASE ── */
* { margin:0; padding:0; box-sizing:border-box; }
body {
min-height: 100vh;
background: #0a0a0f;
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, sans-serif;
}
/* ── FADE-IN ANIMATION ── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── CARD ── */
.card {
background: #18181b;
border: 1px solid #2e2e35;
border-radius: 20px;
padding: 36px 32px;
width: 300px;
text-align: center;
transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.35s ease;
/* Stagger: fade in the card itself */
opacity: 0;
animation: fadeUp 0.6s ease 0.1s forwards;
}
.card:hover {
transform: translateY(-10px);
box-shadow: 0 30px 60px rgba(0,0,0,0.5);
}
/* ── AVATAR with PULSE ── */
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(250,92,123,0.6); }
70% { box-shadow: 0 0 0 14px rgba(250,92,123,0); }
100% { box-shadow: 0 0 0 0 rgba(250,92,123,0); }
}
.avatar-wrap {
display: inline-block;
margin-bottom: 20px;
opacity: 0;
animation: fadeUp 0.6s ease 0.25s forwards;
}
.avatar {
width: 80px; height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #fa5c7b, #a78bfa);
display: flex; align-items: center; justify-content: center;
font-size: 2rem;
animation: pulse-ring 2s ease-out infinite;
}
/* ── NAME & BIO ── */
.name {
font-size: 1.3rem;
font-weight: 700;
color: #f4f4f5;
margin-bottom: 6px;
opacity: 0;
animation: fadeUp 0.6s ease 0.35s forwards;
}
.bio {
font-size: 0.88rem;
color: #71717a;
margin-bottom: 20px;
opacity: 0;
animation: fadeUp 0.6s ease 0.45s forwards;
}
/* ── BADGE ── */
@keyframes badge-pulse {
0%,100% { opacity: 1; }
50% { opacity: 0.5; }
}
.badge {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(250,92,123,0.15);
border: 1px solid rgba(250,92,123,0.4);
color: #fa5c7b;
padding: 4px 12px;
border-radius: 50px;
font-size: 0.78rem;
margin-bottom: 24px;
opacity: 0;
animation: fadeUp 0.6s ease 0.5s forwards;
}
.badge-dot {
width: 6px; height: 6px;
background: #fa5c7b;
border-radius: 50%;
animation: badge-pulse 1.2s ease infinite;
}
/* ── BUTTON with SHINE ── */
@keyframes shine {
from { left: -100%; }
to { left: 150%; }
}
.btn {
display: block;
background: #fa5c7b;
color: #fff;
border: none;
padding: 12px 28px;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, background 0.2s;
opacity: 0;
animation: fadeUp 0.6s ease 0.6s forwards;
}
.btn::after {
content: '';
position: absolute;
top: -50%; left: -100%;
width: 60%; height: 200%;
background: rgba(255,255,255,0.25);
transform: skewX(-20deg);
transition: none;
}
.btn:hover::after {
animation: shine 0.5s ease;
}
.btn:hover {
background: #e04460;
transform: scale(1.03);
}
/* ── STATS ── */
.stats {
display: flex;
justify-content: space-around;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #2e2e35;
opacity: 0;
animation: fadeUp 0.6s ease 0.7s forwards;
}
.stat-value { font-size: 1.2rem; font-weight: 700; color: #f4f4f5; }
.stat-label { font-size: 0.72rem; color: #71717a; margin-top: 2px; }
</style>
</head>
<body>
<div class="card">
<div class="avatar-wrap">
<div class="avatar">🚀</div>
</div>
<div class="name">Your Name</div>
<div class="bio">CSS Animator · Creative Coder</div>
<div class="badge">
<span class="badge-dot"></span>
Online
</div>
<button class="btn">View profile →</button>
<div class="stats">
<div>
<div class="stat-value">42</div>
<div class="stat-label">Projects</div>
</div>
<div>
<div class="stat-value">128</div>
<div class="stat-label">Followers</div>
</div>
<div>
<div class="stat-value">7</div>
<div class="stat-label">Awards</div>
</div>
</div>
</div>
</body>
</html>The aha moment
Open the file in your browser — every element fades in with a delay, the avatar pulses, the card rises on hover, the button lights up. All pure CSS, zero JavaScript. That is the power of @keyframes + transitions combined!
Bonus challenges
If you fancy it, try these extensions:
- → Swap the emoji for a real photo (
background-image) - → Add a shimmer skeleton that appears 1 second before the card
- → Animate the stat numbers when hovering over the card (CSS counter + animation)
- → Build a dark/light toggle with
:checked+ CSS (no JS!)
Chapter 08
Cheat sheet
The cheat sheet below, as a signpost.
transition@keyframes + animationtransform