Motion helps people understand what is happening on a page: a button that responds to hover feels clickable, a menu that slides in shows where it came from, and a spinner shows that something is loading. Two tools handle motion: CSS transitions for simple changes between two states, and animations for multi-step or repeating motion.
CSS transitions: animating a change
Normally, when a property changes (for example on :hover), the new value appears instantly. A transition makes the browser animate from the old value to the new one over time. You add the transition property to the element in its normal state, not the hover state, so it animates both ways.
| Property | Example | Meaning |
|---|---|---|
transition-property | background-color | Which property to animate |
transition-duration | 200ms | How long it takes |
transition-timing-function | ease-out | The speed curve (easing) |
transition-delay | 0.1s | Wait before starting |
transition | transform 200ms ease-out | Shorthand for all of the above |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hover and focus transitions</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; display: flex; gap: 24px; flex-wrap: wrap; }
.btn {
background: #1971c2;
color: white;
border: 0;
padding: 12px 20px;
border-radius: 8px;
font: inherit;
cursor: pointer;
transition: background-color 200ms ease-out, transform 200ms ease-out;
}
.btn:hover,
.btn:focus-visible {
background: #1864ab;
transform: translateY(-2px);
}
.card {
width: 200px;
padding: 20px;
border-radius: 12px;
background: white;
box-shadow: 0 1px 3px rgb(0 0 0 / 20%);
transition: transform 250ms ease, box-shadow 250ms ease;
}
.card:hover {
transform: scale(1.04);
box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
}
</style>
</head>
<body>
<button class="btn">Reserve a table</button>
<div class="card">Hover me: Chettinad thali</div>
</body>
</html>Avoid transition: all. It animates every property that changes, including ones you did not intend, and makes performance harder to reason about. List the properties you actually want to animate.
Easing and duration
Easing describes how speed changes during the motion. ease-out starts fast and slows down, which feels natural for things entering or responding to the user. ease-in is better for things leaving. linear suits spinners and progress bars. For precise control use cubic-bezier(), which design tools like Figma can export.
Keep interface motion short. Hover and focus effects feel snappy at around 100 to 250ms; larger panels may take 300 to 400ms. Anything longer makes the interface feel slow.
Keyframe animations
For motion that has several steps, repeats, or starts on its own without a state change, use an animation. First define the steps with @keyframes and a name, using from/to or percentages. Then attach it to an element with the animation property.
animation-nameandanimation-durationare the two you always need.animation-iteration-count: a number orinfinite.animation-direction: alternateplays forwards then backwards.animation-fill-mode: bothkeeps the first frame before it starts and the last frame after it ends.- The shorthand:
animation: spin 1s linear infinite;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keyframes: fade-in, spinner and pulse</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; display: grid; gap: 30px; justify-items: start; }
@keyframes fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
to { transform: rotate(1turn); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
.message {
animation: fade-up 600ms ease-out both;
background: #ebfbee;
padding: 16px;
border-radius: 8px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #dee2e6;
border-block-start-color: #e8590c;
border-radius: 50%;
animation: spin 900ms linear infinite;
}
.dot {
width: 16px;
height: 16px;
background: #e03131;
border-radius: 50%;
animation: pulse 1.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.message, .dot { animation: none; }
.spinner { animation-duration: 3s; } /* slower, still shows activity */
}
</style>
</head>
<body>
<div class="message">Your order from Hotel Saravana has been placed.</div>
<div class="spinner" role="status" aria-label="Loading"></div>
<div class="dot" aria-hidden="true"></div>
</body>
</html>Transition or animation?
| Use a transition when… | Use an animation when… |
|---|---|
| A property changes between two states, such as hover, focus or an added class | The motion has more than two steps |
| The change is triggered by the user or by JavaScript | It should start on its own when the page loads |
| It should reverse smoothly when the state changes back | It should repeat or loop, like a spinner |
| Examples: button hover, menu open, colour change | Examples: loading indicator, attention pulse, entrance effect |
In practice, most interface motion is built with CSS transitions. Keyframe animations are the spice: use them for a few deliberate moments rather than on every element.
Keep animations smooth
Browsers can animate transform and opacity very cheaply because they do not change the page layout. Animating properties like width, height, top or margin forces the browser to recalculate the layout on every frame, which can stutter on low-cost phones. To move something, use transform: translate(); to grow it, use scale(); to fade it, use opacity. The post CSS Animation Performance Tips goes further.
Respect reduced motion
Some people get dizzy, nauseous or distracted by motion, for example people with vestibular disorders. Operating systems let them switch on a “reduce motion” setting, and CSS can read it with the prefers-reduced-motion media query. When it is reduce, remove or tone down non-essential motion such as large movements, parallax and looping effects. Gentle fades are usually acceptable.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A motion-safe approach</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; }
.banner {
background: #fff0f6;
padding: 20px;
border-radius: 10px;
}
/* Only add movement when the user has not asked for less motion */
@media (prefers-reduced-motion: no-preference) {
.banner {
animation: slide-in 700ms ease-out both;
}
}
@keyframes slide-in {
from { transform: translateX(-40px); opacity: 0; }
to { transform: none; opacity: 1; }
}
</style>
</head>
<body>
<div class="banner">Pongal offers start this Friday.</div>
<p>Turn on "reduce motion" in your operating system settings and reload: the banner appears without sliding.</p>
</body>
</html>Tip: Wrapping motion in @media (prefers-reduced-motion: no-preference), as in the last example, is a safe default: motion is an extra that only users who are fine with it receive.
Watch out: Never put essential information only inside an animation, and avoid anything that flashes more than three times a second. Rapid flashing can trigger seizures and fails WCAG.
You can also trigger CSS transitions and animations from JavaScript by adding a class when the user clicks; see JS Events. In the final lesson, CSS Variables, you will store durations and easings as reusable tokens.
