What you'll learn
- Quick answer
- What are CSS animations?
- Transitions vs animations: the difference
- Transitions: animating a state change
- @keyframes: defining the steps
- The animation shorthand explained
- Timing functions and looping
- Demo 1: a loading spinner
- Demo 2: a hover pulse button
- Performance, accessibility, and a recommendation
- FAQ
Quick Answer
CSS animations let you move and change elements over time using @keyframes to define the steps and the animation property to play them. Transitions are the simpler cousin: they animate a single change between two states, usually on hover or focus. Use transitions for quick state changes and @keyframes animations for multi-step or looping motion like spinners. It all runs in pure CSS, with no JavaScript needed.
What are CSS animations?
CSS animations let you move, fade, scale, and recolour elements right in your stylesheet, with no JavaScript required. If you have ever seen a loading spinner, a button that gently pulses, or a menu that slides open, that motion is usually pure CSS.
There are two tools for the job, and beginners often mix them up:
- Transitions animate a change between two states, for example a button changing colour when you hover over it.
- Keyframe animations (the
@keyframesrule plus theanimationproperty) can move through many steps and loop forever, which is perfect for a spinner.
In this tutorial you will learn both, understand when to reach for each, and build two real demos: a loading spinner and a hover pulse. Everything here works in every modern browser.
Transitions vs animations: the difference
Before we write any code, here is the quick mental model. A transition reacts to something changing. An animation can play all on its own.
| Feature | Transition | Keyframe animation |
|---|---|---|
| Needs a trigger (hover, focus, class change) | Yes | No, can autoplay |
| Multiple steps (0%, 50%, 100%) | No | Yes |
| Can loop forever | No | Yes |
| Best for spinners and progress | No | Yes |
| Best for hover colour changes | Yes | Overkill |
In short: reach for a transition when one property changes between two states, and a keyframe animation when you need several steps or continuous looping.
Transitions: animating a state change
A transition tells the browser to ease smoothly from an old value to a new value instead of snapping instantly. You put the transition property on the normal state, then define the new value somewhere else, usually a :hover or a toggled class.
.box {
background: #6c5ce7;
transition: background 0.3s ease;
}
.box:hover {
background: #a29bfe;
}The shorthand reads as: which property, how long, and which timing function. You can animate more than one property by separating them with commas:
.box {
transition: background 0.3s ease, transform 0.2s ease-out;
}Gotchas to remember:
- A transition only fires when the value changes, so you always need a trigger.
- Name specific properties instead of
transition: all. Usingallis convenient but can animate things you did not intend and hurt performance. - You cannot transition
display: nonetodisplay: block. Animateopacityandvisibilityinstead.
@keyframes: defining the steps
When you need more than a start and an end, you write a @keyframes rule. It has a name and a series of steps. Each step describes what the element should look like at that point in the timeline.
The simplest form uses from and to:
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}For more control, use percentages. 0% is the start and 100% is the end:
@keyframes slide-up {
0% { transform: translateY(20px); opacity: 0; }
60% { opacity: 1; }
100% { transform: translateY(0); opacity: 1; }
}Defining the keyframes does nothing on its own. It is just a named recipe. To actually play it, you attach it to an element with the animation property, which we cover next.
The animation shorthand explained
The animation property is a shorthand for several sub-properties. You can set them separately, but the shorthand is much shorter once you know the order.
.card {
animation: slide-up 0.5s ease-out 0s 1 normal forwards;
}
/* Same thing, written the long way */
.card {
animation-name: slide-up;
animation-duration: 0.5s;
animation-timing-function: ease-out;
animation-delay: 0s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
}Here is what each part does:
- name: which
@keyframesrule to play. - duration: how long one cycle takes, like
0.5sor500ms. This is required, or nothing animates. - timing-function: the speed curve (see the next section).
- delay: how long to wait before starting.
- iteration-count: a number, or
infiniteto loop forever. - direction:
normal,reverse, oralternateto bounce back and forth. - fill-mode:
forwardskeeps the final frame after it ends, so the element does not snap back.
The two values that must be present for anything to happen are the name and the duration. The rest are optional.
Timing functions and looping
The timing function controls the pace of the motion, not the total time. The built-in keywords cover most needs:
linear: constant speed, no acceleration. Best for spinners.ease(the default): starts slow, speeds up, slows down.ease-in: starts slow.ease-out: ends slow.ease-in-out: both.
For custom curves, use cubic-bezier(), and for stop-motion style jumps use steps():
animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
animation-timing-function: steps(4, end);To loop, set animation-iteration-count: infinite. Pair it with animation-direction: alternate so the animation plays forward, then backward, giving a smooth back-and-forth instead of a hard reset each cycle:
.breathe {
animation: pulse 1.5s ease-in-out infinite alternate;
}
Demo 1: a loading spinner
A spinner is the classic keyframe animation: one continuous, evenly paced, looping rotation. We only need a circle with one coloured edge, spun forever.
The HTML is a single element:
<div class="spinner"></div>And the CSS:
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #6c5ce7;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}A few things make this work well. The grey border with one border-top-color gives the spinner its gap. We use linear so the speed stays constant (an ease spinner looks like it stutters). And because the start is just the element's normal state, a single to keyframe is all we need.
Demo 2: a hover pulse button
Now a multi-step animation triggered by hover. We want the button to grow slightly, shrink back, and repeat gently while the pointer is over it.
<button class="pulse-btn">Enrol now</button>.pulse-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: #6c5ce7;
color: #fff;
cursor: pointer;
}
.pulse-btn:hover {
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1); }
}Because the animation lives inside :hover, it starts when the pointer arrives and stops when it leaves. The 50% keyframe is the peak of the pulse, and returning to scale(1) at 100% makes each loop seamless. We animate transform rather than width and height, which brings us to the final section.
Performance, accessibility, and a recommendation
Two habits will keep your animations smooth and considerate.
Animate transform and opacity
Browsers can move and fade elements very cheaply using transform and opacity. Animating width, height, top, or margin forces the page to recalculate layout on every frame, which can look janky on phones. Whenever you can, use transform: scale(), translate(), or rotate() instead.
Respect reduced motion
Some people feel dizzy or unwell from motion, and their device carries a setting for it. Honour it with a media query:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}The recommendation: use a transition for simple, one-off state changes like hovers and toggles, and save @keyframes animations for multi-step or looping motion like spinners and pulses. Keep durations short (0.2s to 1s feels natural), animate transform and opacity, and always add the reduced-motion query.
Want to practise these step by step with exercises? Work through our free CSS course, which covers selectors, layout, and animations from the ground up.
Frequently Asked Questions
What is the difference between a CSS transition and a CSS animation?
A transition animates a single change between two states and needs a trigger, such as a hover or a class being added. A keyframe animation can define many steps with @keyframes, can start on its own, and can loop forever. Use transitions for quick state changes and animations for multi-step or looping motion.
Do I need JavaScript for CSS animations?
No. Both transitions and @keyframes animations run entirely in CSS. You only need JavaScript if you want to start, stop, or change an animation in response to logic, for example toggling a class after a form is submitted.
How do I make a CSS animation loop forever?
Set animation-iteration-count: infinite, or add infinite in the animation shorthand. To make it play forward then backward smoothly, also add animation-direction: alternate so it does not snap back to the start on every cycle.
Why is my CSS animation not working?
The most common reasons are a missing animation-duration (without a duration nothing plays), a keyframe name that does not match the @keyframes rule, or trying to animate a property that cannot be interpolated, like display. Double-check the name and duration first.
Which CSS properties are best to animate for performance?
Stick to transform (scale, translate, rotate) and opacity. Browsers can render these without recalculating page layout, so they stay smooth even on phones. Animating width, height, top, or margin is heavier and can look choppy.
How do I make animations accessible for users sensitive to motion?
Wrap or disable your motion inside a @media (prefers-reduced-motion: reduce) query. When a user has turned on the reduce-motion setting on their device, you can set animation: none and transition: none so the interface stays still and comfortable for them.
