Imagine you're watching a door open. It doesn't just teleport from closed to open — it moves smoothly. That smooth movement is essentially what CSS transitions and animations do to elements on a webpage.
A transition moves an element from one style state to another — smoothly — when triggered by an event (like hovering). You define the start state and end state; CSS fills in the in-between.
An animation lets you define a full sequence of style changes that play automatically, loop, reverse, and more. You script every step using @keyframes.
| Where you see it | What's happening |
|---|---|
| Hover over a link | Transition: color or underline changes smoothly |
| Loading spinner | Animation: element rotates infinitely |
| Dropdown menu opens | Transition: height or opacity fades in |
| Progress bar fills | Transition: width grows over time |
| Notification badge | Animation: scales and pulses on loop |
| Page hero image | Animation: fades in or slides up on load |
A transition watches a CSS property and, when that property changes, it smoothly animates it from the old value to the new value over a specified duration.
:hover, :focus, or JS toggling a class)| Property | What it controls | Example value |
|---|---|---|
transition-property | Which CSS property to animate | background-color |
transition-duration | How long the transition takes | 0.3s |
transition-timing-function | Speed curve (ease in, ease out, etc.) | ease-in-out |
transition-delay | Wait before starting | 0.1s |
transition | Shorthand for all four | all 0.3s ease 0.1s |
/* property duration timing-function delay */ transition: background-color 0.3s ease-in-out 0s; /* multiple transitions */ transition: background-color 0.3s ease, transform 0.2s ease-out;
/* CSS */
.button {
background: #7F77DD;
color: white;
padding: 10px 24px;
border-radius: 8px;
transition: background 0.3s ease, transform 0.2s ease;
}
.button:hover {
background: #534AB7;
transform: scale(1.05);
}
/* CSS */
.card {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.4s ease, box-shadow 0.4s ease;
}
.card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
}
/* Quick way to animate everything */
.element {
transition: all 0.3s ease-in-out;
}
/* WARNING: 'all' can be expensive for performance.
Prefer naming specific properties. */
transition: all will animate every changing property. This can cause unexpected animations on things like height or display. Be specific when you can..nav-item:nth-child(1) { transition: opacity 0.3s ease 0s; }
.nav-item:nth-child(2) { transition: opacity 0.3s ease 0.1s; }
.nav-item:nth-child(3) { transition: opacity 0.3s ease 0.2s; }
transition-duration and transition-delay?Any property with a numeric or color mid-point — things like:
opacity, color, background-colorwidth, height, margin, paddingtransform (translate, scale, rotate)border-radius, font-size, letter-spacingdisplay (block↔none), font-family, or background-image. For display toggling, use opacity + visibility instead.While transitions react to an event, animations run on their own. You define a full timeline of states using @keyframes, and the browser plays it out — automatically, on loop, in reverse, delayed, or paused.
| Feature | Transitions | Animations |
|---|---|---|
| Trigger | Needs an event (hover, click, class) | Runs automatically |
| Keyframes | Only start and end | Unlimited keyframes |
| Looping | No | Yes, controllable |
| Complexity | Simple (A → B) | Complex (A → B → C → D) |
| Use for | Hover/focus states | Loaders, storytelling, onload |
A keyframe rule defines what the element looks like at each stage of the animation. You name it, then reference that name.
@keyframes myAnimation {
0% { opacity: 0; transform: translateY(20px); }
50% { opacity: 0.5; }
100% { opacity: 1; transform: translateY(0); }
}
/* You can also use 'from' and 'to' for simple cases */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
| Property | What it controls | Example |
|---|---|---|
animation-name | Which @keyframes to use | spin |
animation-duration | Length of one cycle | 1s |
animation-timing-function | Speed curve | ease-in-out |
animation-delay | Wait before starting | 0.5s |
animation-iteration-count | How many times to run | infinite or 3 |
animation-direction | Normal, reverse, alternate | alternate |
animation-fill-mode | State before/after playing | both |
animation-play-state | Running or paused | paused |
animation | Shorthand for all | spin 1s linear infinite |
/* name duration timing delay iterations direction fill-mode */ animation: fadeIn 0.5s ease 0.2s 1 normal forwards;
| Value | Behaviour |
|---|---|
none | Snaps back to original style after animation ends |
forwards | Stays at the last keyframe state |
backwards | Applies first keyframe style during delay |
both | Combines forwards and backwards behaviour |
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px; height: 40px;
border: 4px solid #eee;
border-top-color: #534AB7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-text {
animation: slideUp 0.6s ease-out both;
}
.hero-subtitle {
animation: slideUp 0.6s ease-out 0.2s both; /* delayed */
}
@keyframes bounce {
from { transform: translateY(0); }
to { transform: translateY(-40px); }
}
.ball {
animation: bounce 0.8s cubic-bezier(0.36,0.07,0.19,0.97)
infinite alternate;
}
alternate direction makes the animation ping-pong: it plays forward, then backward, creating a natural bounce without needing to define a return keyframe.animation-fill-mode: forwards do, and when would you use it?Duration: 2s
[0%]──────[25%]──────[50%]──────[75%]──────[100%]
│ │ │ │ │
opacity:0 opacity:.5 opacity:1 opacity:.5 opacity:0
← defined by @keyframes at each stop →
Each ball below starts at the left and reaches the right in exactly 3 seconds — but travels differently. Watch how each timing function moves:
linear — constant speedease — fast start, smooth end (default)ease-in — slow start, fast endease-out — fast start, slow endease-in-out — slow start AND slow end| Function | Best for |
|---|---|
linear | Spinners, infinite loops, progress bars |
ease | General transitions — feels most natural |
ease-in | Elements that exit the screen (accelerating away) |
ease-out | Elements that enter the screen (decelerating in) |
ease-in-out | Attention-grabbing effects, modal transitions |
cubic-bezier() | Custom physics — bounces, springs, over-shoots |
Do you need it to run automatically (without user action)?
YES → Use an Animation (@keyframes)
NO → Is the change triggered by hover/focus/class toggle?
YES → Use a Transition
NO → Trigger a CSS class change with JS, then Transition
/* Overshoot / spring effect */ animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* Fast snap */ animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); /* Use https://cubic-bezier.com to build custom curves visually */
These run on the GPU and are the fastest to animate:
transform (translate, scale, rotate, skew)opacitywidth, height, top, left, margin, or padding — these trigger layout recalculation ("reflow") and are expensive. Use transform: translate() instead of changing top/left.Try these in a code editor (CodePen, VS Code with Live Server). Reveal hints when you're stuck.
Create a button that transitions its background from blue to purple over 0.5s when hovered.
transition: background-color 0.5s ease; on the base style. Define :hover { background-color: ... }. Only the property you're changing needs to be set on :hover.Make a nav link whose underline grows from 0 to 100% width on hover using a ::after pseudo-element.
::after { content:''; display:block; height:2px; width:0; transition: width 0.3s ease; } and then on a:hover::after { width: 100%; }Make an icon rotate 360° over 0.5s when you hover over it.
transition: transform 0.5s ease; on the base. Then on :hover { transform: rotate(360deg); }Make a heading appear by fading in from opacity 0 to 1 over 1 second when the page loads.
@keyframes fadeIn { from{opacity:0} to{opacity:1} } then .heading { animation: fadeIn 1s ease both; }Create 3 dots that fade in and out with a staggered delay to form a loading indicator.
@keyframes pulse that animates opacity 1→0→1. Give the 2nd dot animation-delay: 0.2s, 3rd dot 0.4s. Set animation-iteration-count: infinite.Create a hamburger icon (3 bars) that animates into an × when a class open is toggled. The top/bottom bars rotate, the middle fades out.
<span> inside a button. On .open span:first-child { transform: rotate(45deg) translate(5px,5px); }, middle: opacity:0, last: rotate(-45deg). Add transitions to each span.Build a card that flips over on hover to reveal a back face, using perspective and rotateY.
transform-style: preserve-3d. Back face gets transform: rotateY(180deg) and backface-visibility: hidden. On hover: .card { transform: rotateY(180deg); }Cards start invisible (opacity:0; transform: translateY(40px)). When a JS class visible is added (use IntersectionObserver), they animate in.
transition: opacity 0.6s ease, transform 0.6s ease; to the cards. The .visible class sets opacity:1; transform:translateY(0). Use IntersectionObserver to add the class.Build a progress bar that animates from 0% to a target value using @keyframes and animation-fill-mode: forwards.
@keyframes grow { from{width:0} to{width:75%} } on the inner fill bar. Use animation: grow 1.5s ease-out forwards; so it stays at 75% after finishing.Make text appear letter by letter using steps() timing function and animating width + overflow: hidden.
@keyframes type { from{width:0} to{width:100%} }. Apply to a fixed-width container: animation: type 3s steps(30,end) both; overflow:hidden; white-space:nowrap;. Count the characters and match the steps number.Build a hero section where: the background gradient shifts slowly (animation), the heading slides up on load (animation), nav links underline on hover (transition), and a CTA button scales + changes colour on hover (transition). Respect prefers-reduced-motion.
Create a full-screen loading animation using only CSS. Try: a morphing blob, a bouncing logo, or staggered text letters, then fade out the screen once a class is toggled.
left/top/width/height instead of transform. Causes jank and reflows.transform: translate() and transform: scale() instead.:hover means the return transition is instant.transition on the base class, not the hover state.transition: all carelessly — can unintentionally animate things like height: auto (which can't be transitioned).animation-fill-mode — the element snaps back to its original style the moment the animation ends.animation-fill-mode: both or forwards for elements that should stay put.will-change: transform to hint the GPU, use fewer simultaneous animations.prefers-reduced-motion — animations can cause nausea or headaches for users with vestibular disorders.transition on the base element?@keyframes name spelled exactly right?| Change type | Cost | Examples |
|---|---|---|
| Layout (Reflow) | Expensive | width, height, margin, top |
| Paint | Moderate | color, background, border |
| Composite | Cheap (GPU) | transform, opacity |
/* Always include this for accessibility */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
| Situation | Use |
|---|---|
| Button hover state | Transition |
| Loading indicator | Animation |
| Form field focus ring | Transition |
| Hero text slide-in on load | Animation |
| Dropdown menu open/close | Transition |
| Pulsing notification badge | Animation |
| Card hover lift | Transition |
| Background colour shift (ambient) | Animation |
transition on the base element@keyframesfill-mode: both to prevent snap-backtransform + opacitywill-change sparinglyprefers-reduced-motionTap any card to reveal the answer.