A keyframe saves what an element looks like at one moment of an animation. You write them inside an @keyframes rule, and the browser calculates every state between the ones you set.

A CSS transition has a start state and an end state and nothing else. A keyframes rule holds as many in-between states as you like, and it plays once an element’s animation-name property points at the rule’s name.

Developers mostly use it for loaders and entrance effects. The W3C CSS Animations Level 1 draft (2023) says the timing function applies between each pair of adjacent keyframes. With the default ease, that means the animation slows down at every keyframe, not once across the whole run.

What are CSS keyframes?

The @keyframes at-rule describes one cycle of motion and nothing else. Duration, repetition and direction sit in the animation properties, which makes the rule only half of a CSS animation.

@keyframes pulse {
0%   { transform: scale(1); }
50%  { transform: scale(1.15); }
100% { transform: scale(1); }
}

.badge {
animation: pulse 2s ease-in-out infinite;
}

The pulse above sets keyframes at 0%, 50% and 100%, and the browser interpolates the scale values between them on every frame. MDN’s reference calls each keyframe a waypoint, and that is a fair way to picture it.

On its own the rule does nothing. An element has to point animation-name at it first.

The W3C’s CSS Animations Level 1 draft (March 2023) separates keyframes from transitions by trigger. Transitions fire implicitly when a property value changes, while animations run explicitly once the animation properties are applied.

Writing a @keyframes rule: name, selectors and missing values

A rule needs a case-sensitive name, then a set of selectors that each carry their own block of styles.

The browser matches the name against animation-name and sorts the selectors by position on the timeline. After that it animates each property across whichever keyframes include it.

Naming the rule

Case matters. Names are compared codepoint by codepoint, so FOO and foo are different rules. The words none and initial are invalid as bare names, though both work as quoted strings (W3C CSS Animations Level 1, 2023).

When two rules share a name, the last one in document order wins and the earlier ones are ignored.

Is responsive design still a top priority?

Explore the latest responsive design statistics: adoption rates, performance impact, user behavior, and trends shaping modern websites.

See the Numbers →

I name rules for the motion, not the look. fade-in survives a redesign. blue-box-slide doesn’t, the first time someone changes the color.

Keyframe selectors

Each selector places one keyframe on the timeline.

  • from is the same as 0%, and to is the same as 100%.
  • Percentages need the unit. A bare 0 is invalid.
  • Any block placed below 0% or above 100% gets ignored.
  • A comma list such as 25%, 75% applies one block to both points.

Typing percentages by hand gets tedious past five or six keyframes. A keyframe animation generator writes the same rule for you.

Missing values and duplicate percentages

Gaps in a rule do not break it. The browser fills them from other keyframes or from the element’s own styles.

SituationWhat the browser does
No 0% or from keyframeBuilds one from the element’s computed values
Property missing from a middle keyframeAnimates it across the keyframes that do include it
Same percentage twice in one ruleMerges the declarations, later values win per property
Same name in two rulesUses the last rule only, no merging
!important on a declarationIgnores that declaration

MDN’s worked example covers the middle case. There, top animates across the 0%, 30% and 100% keyframes, while left uses 0%, 68%, 72% and 100% (MDN, updated April 2026).

One-sided rules follow from the same behavior. A lone to { opacity: 0 } block animates an element from its current opacity, which makes a quick reusable fade-out.

Applying keyframes with the animation properties

Nothing moves until animation-name matches the rule’s name and animation-duration is above 0s. The duration defaults to 0s, and a zero-length animation with the default fill mode has no visible effect (W3C CSS Animations Level 1, 2023).

  1. Write the @keyframes rule and give it a name.
  2. Set animation-name on the element to that name.
  3. Set animation-duration to a value above 0s.
  4. Add timing, delay, iteration count, direction and fill mode wherever the default behavior is wrong.

Repeating, reversing, holding and pausing

Repetition is controlled by animation-iteration-count, which defaults to 1 and accepts any non-negative number or infinite. A fractional value ends the animation partway through a cycle.

With animation-direction set to alternate, odd cycles play forward and even cycles in reverse. Give it a count of 1 and it looks the same as normal.

animation-fill-mode is the one that surprises people. The default, none, snaps the element back after the end. forwards holds the last keyframe, backwards applies the first keyframe during a delay, and both does each.

Setting animation-play-state to paused freezes the animation at its current progress. Running resumes it from there.

Run a 360-degree rotation with linear timing on infinite and you get a CSS spinner, about the most typical looping keyframes animation there is. Negative delays work too, and they start the animation partway through its first cycle.

Timing functions apply between keyframes

The timing function runs once per interval, not once across the whole animation.

The W3C draft defines progress as the share of time elapsed between the current keyframe and the next one. So the default ease starts and ends slowly in every segment, and a 5-keyframe animation with ease eases in and out 4 separate times.

Declare animation-timing-function inside a keyframe to control the move to the next one. A value on the 100% keyframe is ignored because no interval follows it.

Reverse playback flips the curve too, so an ease-in rule runs as ease-out when the direction is reversed. Custom curves written with cubic-bezier() follow the same per-interval logic, and steps() turns each interval into discrete jumps.

The animation shorthand

Order mostly doesn’t matter in the shorthand, with a couple of exceptions that cause real confusion.

  • The first time value is the duration and the second is the delay, so 2s 500ms and 500ms 2s are not the same.
  • A keyword that is valid for another animation property gets claimed by that property first. A rule named backwards needs animation-name written out in full.

Several animations go in one comma-separated list. When two of them animate the same property, the one listed last wins.

The shorthand also resets every longhand it leaves out. That is fine in a first declaration and a bug when a later rule only wants to change the duration. That one has cost me an afternoon.

Which properties can CSS keyframes animate?

Almost every CSS property can be animated. What differs is the result. Lengths and colors blend smoothly, but values that cannot blend flip at the halfway point.

MDN’s animatable properties reference (updated November 2025) says all CSS properties are animatable unless stated otherwise, and each property’s animation type decides how two values combine.

Smooth versus stepped changes

Interpolated values blend frame by frame between keyframes. That covers lengths, colors, numbers and transform functions, so width, opacity, background-color and transform all behave this way.

Anything that cannot blend is discrete. It holds the start value until progress reaches 50%, then switches to the end value (MDN, 2025). Values whose components do not match in number or type fall back to discrete as well.

That halfway switch is why a keyword property between two keyframes looks like a jump, not a fade.

Custom properties and @property

As of MDN’s May 2026 update, @property is Baseline 2024 (newly available since July 2024).

Unregistered custom properties animate as discrete values, so a plain –progress variable inside keyframes jumps at 50%. Registering the property gives it a type, and the browser then interpolates it by computed value.

The registration itself is picky.

  • syntax and inherits are both required, or the whole rule is ignored.
  • initial-value is required unless syntax is the universal “*”.

MDN’s own demo animates a registered progress property to move a gradient color stop, which produces a progress bar. Without the registration, the same keyframes would not blend.

What keyframes ignore

A property that cannot be animated is skipped, and the rest of the keyframe still runs.

MDN’s @keyframes page says unsupported properties are ignored while the supported ones still animate. An animation whose keyframes hold nothing animatable changes nothing on screen, though the W3C draft says any animation with a valid @keyframes rule still runs and fires events, even an empty one.

SVG properties such as fill and stroke-dashoffset appear in MDN’s CSS property reference, so keyframes drive them like any other value. The guide to Animating SVG with CSS uses the same rule structure described above.

CSS keyframes vs transitions vs the Web Animations API

A transition covers a simple two-state change. Keyframes take over once the motion has several steps or needs to loop, and the Web Animations API is for cases where script decides when the animation starts.

OptionStatesLoop controlBest fit
CSS transition2, start and endNoneHover, focus, state changes
CSS keyframesAny numberanimation-iteration-countLoaders, entrances, multi-step effects
Web Animations APIAny numberiterations optionPlayback driven by script

Why transitions suit hover and focus

MDN’s animation performance guide (updated November 2025) notes that a new transition starts from the current style immediately, even mid-transition, instead of jumping to the end state.

That makes transitions the better fit for hover and focus feedback, where users leave before the effect finishes.

For a plain hover effect, a CSS transition generator produces the declaration, and keyframes add nothing.

When keyframes earn their extra syntax

Keyframes pay off when motion has to start on its own at load, with no state change to react to. They also cover anything that repeats, forever or a set number of times, and anything that passes through more than two states.

What you give up is control from script. Starting or reversing a keyframes animation, or seeking within it, means toggling classes or properties.

Using the Web Animations API

Element.animate() takes an array of keyframe objects plus timing options and returns an Animation object (MDN, December 2025).

Pass a single keyframe and the browser treats it as the end state, inferring the start from the element’s current computed style. Options such as iterations: Infinity cover looping.

Reach for it when script owns the timing or the values come from runtime data. If a class toggle and a CSS rule would do the job, skip it. MDN’s guide recommends CSS wherever possible and JavaScript for the complex cases.

Scroll-driven animations reuse the same keyframes

The default timeline for a keyframes animation is the document’s clock. animation-timeline swaps it for a scroll() or view() timeline, and the @keyframes rule stays identical (MDN, September 2026).

The animation shorthand sets animation-timeline back to auto, so animation-timeline has to be declared after the shorthand or it does nothing.

The property is not Baseline yet, so treat it as an enhancement. For scroll effects beyond a single timeline, see the techniques for CSS animations on scroll.

Performance limits of animating with keyframes

Animate transform and opacity whenever the effect allows it. web.dev’s compositing guidance lists those two as the only properties the compositor can handle alone, with no layout or paint.

MDN’s animation performance guide (updated November 2025) explains why. Properties that avoid reflow and repaint can be sampled off the main thread, and a promoted layer can animate transform on the GPU.

Transform and opacity against layout properties

Transform and opacity skip layout and paint on every frame, and the work can leave the main thread. The costs are smaller. Each animated element needs its own layer, which uses memory, and neighbors do not reflow, so the layout space stays where it was.

Width, height, top, left and margin go the other way. Surrounding content reflows as the box changes, which is sometimes exactly what you want. It also means reflow and repaint on every frame, with style sampling stuck on the main thread.

I default to transform and opacity and only move to the others when the surrounding layout has to respond.

Using will-change sparingly

will-change is a last resort, not a default.

MDN (updated October 2026) warns that overuse costs memory and extra rendering work, and web.dev adds that on devices with limited memory the layers can outweigh any gain.

  • Add it before the animation starts, not inside @keyframes, since animated properties already count as included.
  • For occasional changes, switch it on and off with script.
  • will-change: opacity creates a stacking context up front, which is easy to forget.
  • Check the layer count in Chrome DevTools before adding more.

CSS keyframes versus JavaScript on the main thread

Sources disagree on which is faster, and the disagreement has a clean explanation.

MDN’s own test in Firefox found CSS animations and requestAnimationFrame callbacks performing almost the same, because both resample styles on the main thread before each repaint. The same page notes that libraries such as GSAP and Velocity.js claim better performance than native CSS.

Both can hold. CSS pulls ahead only when the animated properties qualify for off-main-thread work, which is why MDN still recommends CSS where possible.

Reduced motion and accessible keyframe animations

Wrap large motion in a prefers-reduced-motion query and replace it with a fade or remove it. The media feature reports an operating-system setting that asks for less non-essential motion (MDN, June 2026).

Because prefers-reduced-motion is a media feature, it sits in ordinary media queries and follows source order, so a later rule with the same specificity wins.

Opt-out and opt-in versions

/* Opt out: remove motion when the user asks */
.badge { animation: pulse 2s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
.badge { animation: none; }
}

/* Opt in: add motion only when there is no preference */
@media (prefers-reduced-motion: no-preference) {
.banner { animation: slide-in 600ms ease-out; }
}

Opt-in is the safer default, because anything that fails to match leaves the page still.

MDN’s own example goes the other way and swaps a scaling pulse for a slow dissolve, which keeps a visual cue without the movement.

Which motion causes trouble

Scaling or panning large objects can trigger discomfort for people with vestibular motion disorders (MDN).

W3C’s definition of motion animation under WCAG 2.2 excludes color and opacity changes that do not alter perceived size, shape or position (W3C, 2025). A fade is therefore a safe replacement for a slide or a zoom.

WCAG criteria that apply to keyframes

Success criterion 2.2.2, Pause, Stop, Hide (Level A), applies to movement that starts automatically, lasts more than 5 seconds and sits beside other content. That kind of movement needs a way to pause, stop or hide it, unless the movement is essential (W3C, 2026).

Criterion 2.3.1, Three Flashes or Below Threshold (Level A), allows no more than 3 flashes in any one-second period unless the flashes stay below the general and red flash thresholds (W3C, 2025).

2.3.3, Animation from Interactions, sits at Level AAA. Non-essential motion triggered by an interaction can be turned off (W3C, 2025).

An infinite loop that starts on load passes the five-second mark by definition, so it needs a control when it sits beside other content. The control can be as small as one declaration, switching animation-play-state to paused from a button.

Pausing only while the user keeps focus on the element does not count, because W3C says a pause mechanism must not tie up the user or the focus. A loading spinner that is the only content on the page is exempt.

These criteria cover motion only. The rest of web accessibility (contrast, focus, keyboard access and the rest) needs its own pass.

Why a keyframes animation fails to run

If the name and duration check out, the cause usually sits in the cascade or the element’s display state. A reduced-motion setting on the user’s end is the other common one.

Common causes and fixes

SymptomCauseFix
Animation disappears when a parent is hiddendisplay: none terminates running animations, and they start over when display returnsHide with opacity or visibility, or accept the restart
Value never changesAn !important rule on the element outranks the animationRemove the !important declaration for that property
Edits to @keyframes do not replay the animationUpdating a keyframes rule does not start or restart an animationRemove and re-apply animation-name
Plays on one machine, stays still on anotherThe reduced-motion setting is on and a query overrides the animationToggle the setting while testing
Stalls when the tab is not visibleBrowsers throttle or pause animation updates in hidden tabsExpected behavior, no fix needed

The display, cascade and restart rows come from the W3C CSS Animations Level 1 draft. The motion-setting and background-tab rows come from MDN.

Browser support and the legacy -webkit- prefix

Unprefixed @keyframes is Baseline widely available, so a modern stylesheet needs no vendor copies.

  • @keyframes has been Baseline widely available since September 2015 (MDN, April 2026).
  • prefers-reduced-motion reached the same status in January 2020 (MDN, June 2026).
  • Element.animate() has been widely available since March 2020 (MDN, December 2025).
  • animation-timeline has limited availability and is not Baseline (MDN, September 2026).

Prefixed copies such as @-webkit-keyframes only matter for browsers older than the September 2015 line. Newer features are the real gap, which keeps cross-browser compatibility testing worth doing for animation-timeline.

CSS Keyframes FAQ

Is a CSS keyframe the same as a keyframe in video editing?

The idea matches. Both define a state at a point in time and leave the in-between frames to be calculated. The format differs. CSS keyframes are text rules with percentages, so there is no timeline panel, and the browser interpolates at render time.

Can JavaScript read or change a @keyframes rule?

Yes. The CSSKeyframesRule interface exposes the rule’s name, its keyframes, and the methods appendRule(), deleteRule() and findRule(). findRule() needs the full selector. For a block written as 25%, 75%, findRule(‘75%’) returns null, while findRule(‘25%,75%’) finds it.

Which events does a keyframes animation fire?

animationstart, animationiteration, animationend and animationcancel all fire. animationiteration is skipped when the iteration count is one or less. animationcancel fires when display: none or a changed animation-name removes the animation, and even an empty @keyframes rule fires events.

Can keyframes animate ::before and ::after?

Yes. The animation properties apply to all elements, including the ::before and ::after pseudo-elements. Animation events report the pseudo-element’s name in their pseudoElement attribute, while the event target is the originating element.

How does the browser match several animation names to their durations?

By position. animation-name sets how many animations run, and every other animation property is matched to it from the first value.

A shorter list repeats until it fills the gap, and extra values at the end are ignored.

How can DevTools show whether a keyframes animation is expensive?

Chrome DevTools shows which rendering stage an animation hits. web.dev points to the paint profiler and the layer view for finding repaint work and extra compositor layers.

An animation limited to transform and opacity should show no layout or paint.

When CSS Keyframes Are the Right Tool

Keyframes fit animations that loop or run without script, and anything that passes through three or more states. A two-state change triggered by hover or focus belongs to a transition.

I’d write the animation in a fixed order. Transform and opacity first, since they decide which properties the rule can contain. The prefers-reduced-motion rule goes in next. Profiling in DevTools comes last because it needs a finished animation.

Control is the cost. A running keyframes animation cannot be retargeted mid-flight the way a transition can, so interactions that reverse often need a transition instead.

As of October 2026, animation-timeline is not Baseline, so scroll-driven use of the same keyframes should stay a progressive enhancement until MDN lists it as widely available.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)