CSS Animations Tutorial: Keyframes, Transitions, and Timing

- CSS keyframe animations in one minute
- When should you use a transition instead?
- How does the @keyframes rule work?
- What do the animation properties control?
- Why does an element snap back after the animation?
- How do timing functions work between keyframes?
- Can CSS transition display: none now?
- Which properties should you animate for performance?
- How should reduced motion be implemented?
- A complete accessible card example
- Why is a CSS animation not working?
CSS keyframe animations in one minute
A CSS keyframe animation has two parts: an @keyframes rule that defines property values at points in a sequence, and an animation declaration that assigns the sequence to an element. Use a transition when a property should interpolate after a state change such as :hover. Use keyframes for multiple stops, repetition, reversal or motion that starts without a new state change.
Here is the smallest useful keyframe example:
@keyframes pulse {
from { transform: scale(1); }
to { transform: scale(1.05); }
}
.button {
animation: pulse 600ms ease-in-out infinite alternate;
}
pulse names the sequence. 600ms is one iteration, ease-in-out controls progress within it, infinite repeats it, and alternate reverses every other iteration. The browser interpolates between scale(1) and scale(1.05).
When should you use a transition instead?
A transition responds to a change between an element's old and new computed values. It needs a starting value, a changed value and a property that can transition.
.button {
background: #2563eb;
transition: background 200ms ease;
}
.button:hover,
.button:focus-visible {
background: #1e40af;
}
The shorthand order is:
transition: <property> <duration> <timing-function> <delay>;
Several transitions can be comma-separated:
.card {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition:
transform 250ms ease-out,
box-shadow 250ms ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}
Name the properties rather than using transition: all. all is valid, but it can animate a later property change that was never meant to move.
| Transition property | Purpose | Example |
|---|---|---|
transition-property |
Properties to watch | transform, opacity |
transition-duration |
Time from start to end | 200ms, 0.3s |
transition-timing-function |
Progress curve | ease, cubic-bezier(...) |
transition-delay |
Wait before starting | 100ms |
Seconds and milliseconds are equivalent units: 0.3s equals 300ms. As an editorial starting point, motion under roughly 100ms reads as nearly immediate, many interface transitions fit between 150ms and 400ms, and a 500ms hover can feel slow. Those are design starting points, not platform rules; test the actual interaction.
How does the @keyframes rule work?
The CSS Animations Level 1 specification defines keyframe selectors as percentages from 0% through 100%, with from and to as aliases for the endpoints. A multi-stop sequence looks like this:
@keyframes slide-in {
0% {
opacity: 0;
transform: translateX(-20px);
}
70% {
transform: translateX(4px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.panel {
animation: slide-in 400ms ease-out both;
}
Multiple selectors can share declarations:
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
If a property is missing from the 0% or 100% keyframe, the browser uses its underlying computed value for that endpoint when it can animate the property. That can be convenient, but explicit endpoints are easier to review when another rule later changes the base style.
Keyframe declarations do not participate in the normal cascade in the same way as ordinary style rules. Do not use !important inside @keyframes; the animation specification says those declarations are ignored.
What do the animation properties control?
The familiar Level 1 shorthand represents eight longhands:
| Property | Purpose | Example values |
|---|---|---|
animation-name |
Selects the @keyframes rule |
slide-in |
animation-duration |
Length of one cycle | 600ms, 2s |
animation-timing-function |
Progress within a segment | ease-out, linear |
animation-delay |
Offset before the active interval | 200ms, -200ms |
animation-iteration-count |
Number of cycles | 1, 3, infinite |
animation-direction |
Playback direction | normal, reverse, alternate |
animation-fill-mode |
Effect before or after playback | none, forwards, both |
animation-play-state |
Runs or pauses the animation | running, paused |
The shorthand grammar is flexible, but time values have a fixed interpretation: the first time is duration and the second is delay.
.notice {
animation: slide-in 400ms ease-out 150ms 1 normal both;
}
A duration cannot be negative. Its initial value is 0s, which is a common reason correctly named keyframes appear to do nothing. A delay may be negative. animation-delay: -200ms starts immediately as if the animation had already run for 200 milliseconds; it does not wait a negative amount of wall-clock time.
Why does an element snap back after the animation?
animation-fill-mode controls when the animation effect applies outside its active interval:
noneapplies no effect before or after playback.forwardskeeps the effect at the point where the final iteration ended.backwardsapplies the first relevant keyframe during a positive delay.bothcombinesforwardsandbackwards.
This explains the snap, but forwards should not become a substitute for durable component state. If the final appearance represents a real open, closed, saved or selected state, set that state in the component's ordinary CSS and use animation only for the route between states.
Direction also changes which endpoint counts as the beginning or end. With reverse, the to keyframe supplies the starting effect. With alternate, odd and even iterations run in opposite directions.
How do timing functions work between keyframes?
An easing function maps elapsed time to animation progress. These keywords cover most interface motion:
| Value | Behavior | Common use |
|---|---|---|
linear |
Constant progress rate | Indeterminate spinners |
ease |
Default cubic curve | General transitions |
ease-in |
Slow start | An element leaving |
ease-out |
Slow finish | An element entering |
ease-in-out |
Slow start and finish | Reversing motion |
Custom curves use cubic-bezier(x1, y1, x2, y2). The CSS Easing Functions Level 2 specification requires both X coordinates to remain within 0 and 1; the Y coordinates may fall outside that range.
.modal {
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
steps() produces discrete jumps rather than continuous interpolation. A 20-character reveal can use 20 steps:
@keyframes typing {
from { width: 0; }
to { width: 20ch; }
}
.caption {
width: 20ch;
overflow: hidden;
white-space: nowrap;
animation: typing 2s steps(20) forwards;
}
An animation-timing-function inside a keyframe controls the segment that starts at that keyframe and runs to the next one. This permits different easing on approach and departure without splitting the animation into separate names.
Can CSS transition display: none now?
display is discrete; there are no intermediate display values. CSS Transitions Level 2 introduces transition-behavior: allow-discrete, and @starting-style supplies a before-change style when an element was not previously rendered.
.popover {
display: block;
opacity: 1;
transition:
opacity 200ms ease,
display 200ms allow-discrete;
}
.popover.is-closed {
display: none;
opacity: 0;
}
@starting-style {
.popover {
opacity: 0;
}
}
This is progressive CSS, not permission to assume every target browser implements the same draft feature set. Check support for the browsers in the project, and retain a usable fallback. opacity alone does not remove an element from layout or interaction, so an older fallback may also need visibility, hidden or script-managed state according to the component.
Which properties should you animate for performance?
Start with transform and opacity, then measure. Google's high-performance animation guide recommends checking rendering-pipeline cost before animating other properties because layout- or paint-triggering changes are harder to keep smooth.
Do not turn that into a false guarantee. Whether an effect is composited depends on the browser, element and surrounding styles. Use browser developer tools to inspect frames, layout, paint and layer behavior on representative devices.
Prefer this:
.box {
transform: translateX(0);
transition: transform 300ms ease;
}
.box.is-open {
transform: translateX(240px);
}
over animating left when the intended result is purely visual movement. Width, height, margin and positional properties can affect layout. Sometimes layout really must change; test it rather than disguising a semantic size change with a transform.
will-change is a hint, not a required animation ingredient:
.menu {
will-change: transform;
}
The W3C will-change specification explicitly warns against applying it across too many properties or elements because the optimizations consume resources. Add it only when measurement shows a need, apply it shortly before the change when practical, and remove it after the element stops changing.
Our guides to CSS custom properties and CSS gradients cover two common inputs to animation systems. Keep duration and distance tokens named, but do not assume every custom property will interpolate; registration and the property's value type matter.
How should reduced motion be implemented?
The prefers-reduced-motion media feature has two values: no-preference and reduce. Media Queries Level 5 defines reduce as a request to remove or replace non-essential motion that can cause discomfort or distraction.
The safest default is no transform animation, then opt into it only when no reduced-motion preference is expressed:
.feature-card {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: no-preference) {
.feature-card {
animation: fade-up 500ms ease-out both;
transition: transform 200ms ease, box-shadow 200ms ease;
}
.feature-card:hover,
.feature-card:focus-visible {
transform: translateY(-6px);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
}
}
Do not treat the popular global 0.01ms !important reset as a complete accessibility solution. It can preserve a rapid flash of the same movement, override component behavior and interfere with code that listens for animation completion. Remove or replace non-essential motion deliberately, while keeping essential state feedback available without movement.
WCAG 2.2 adds separate obligations that a media query does not erase. Moving, blinking or scrolling information that starts automatically, lasts more than five seconds and appears alongside other content needs a pause, stop or hide mechanism unless essential. Content must also stay within the three-flashes-or-below-threshold criterion. These are design and testing requirements, not optional polish.
A complete accessible card example
This preserves the original fade-and-lift example while making the static state the baseline:
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feature-card {
opacity: 1;
transform: none;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
@media (prefers-reduced-motion: no-preference) {
.feature-card {
animation: fade-up 500ms ease-out both;
transition: transform 200ms ease, box-shadow 200ms ease;
}
.feature-card:hover,
.feature-card:focus-visible {
transform: translateY(-6px);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
}
}
Keyboard focus receives the same non-essential enhancement as hover. The card remains visible and understandable when animation does not run.
Why is a CSS animation not working?
Check these in order:
- Confirm
animation-nameexactly matches an@keyframesname. - Inspect computed
animation-duration; the initial value is0s. - Confirm the selector actually matches and is not inside an inactive media query.
- Check whether the animated property can interpolate between the supplied values.
- Add explicit
0%and100%values to expose an unexpected underlying style. - Check whether
display: none, DOM removal or a class change cancels the effect. - Inspect
animation-play-state, iteration count, delay and reduced-motion rules. - Use the browser's animation and performance panels instead of judging only by eye.
For transitions, confirm that the base style existed before the new style was applied. If both values arrive in the same style update, there may be no prior change for the browser to transition from.
The durable rule is simple: transitions connect states; keyframes define sequences. Specify the endpoints, parse the shorthand deliberately, measure rendering cost, and design a complete experience for people who request less motion.
An independent publication. Not affiliated with any prior owner of this domain.