Every smooth loading spinner, fading hero image, and sliding menu you’ve seen on the web runs on the same mechanism: CSS keyframes.

Understanding what CSS keyframes are, and how the @keyframes rule actually works, is the difference between copying animation code and writing it with confidence.

This guide covers the full animation sequence model, from basic keyframe syntax and percentage selectors to timing functions, fill modes, GPU compositing, and JavaScript control via the Web Animations API.

By the end, you’ll know exactly which CSS properties to animate, which ones to avoid, and how to keep your keyframe animations smooth across every device.

What Are CSS Keyframes

YouTube player

CSS keyframes are a set of rules that define the intermediate states of an animation sequence. You write them using the @keyframes at-rule, give the block a name, and the browser handles everything in between.

Each keyframe selector maps a percentage of the animation timeline to a specific set of CSS property values. At 0%, the element looks one way. At 100%, it looks another. The browser interpolates all the frames in between automatically.

Without keyframes, CSS has no way to describe multi-step animation sequences. Transitions only handle two states. Keyframes handle as many steps as you need.

The basic syntax looks like this:

“ @keyframes slide-in { from { transform: translateX(-100%); } to { transform: translateX(0); } } `

from and to are just aliases for 0% and 100%. Most real-world animations use percentage values because they allow intermediate steps.

Once you define a keyframe block, it does nothing on its own. You attach it to an element using the animation-name property, and pair it with animation-duration to set how long one cycle runs.

How Does the @keyframes Rule Work

YouTube player

The @keyframes rule assigns a name to an animation sequence and contains one or more keyframe selectors. Each selector is a percentage value or the keywords from/to, followed by a block of CSS declarations.

The browser reads the selectors in order and interpolates property values between each step over the animation duration. Properties not defined in every keyframe are still interpolated when possible. Properties that cannot be interpolated (like display) are dropped entirely.

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 →

How Percentage Selectors Control Animation Timing

Percentage selectors map directly to points in the animation timeline. A 50% selector fires exactly halfway through the animation-duration value.

This matters for multi-step sequences. Consider a three-step animation:

  • 0% — starting state
  • 60% — an overshoot or hold position
  • 100% — final resting state

The browser spends 60% of the total duration getting to the middle step, and only 40% on the final transition. Pacing is entirely controlled by where you place your percentage selectors.

You can also assign the same percentage to two selectors separated by a comma (68%, 72%) to create a hold at that point in the timeline (MDN Web Docs).

What Happens When Keyframe Steps Are Missing

Missing start or end state: If you omit 0% or 100%, the browser uses the element’s existing computed styles as that boundary. This is actually useful. It means you can animate an element away from its natural state without explicitly writing what that state is.

Missing intermediate properties: If a property appears in some keyframes but not others, the browser interpolates it across the steps where it is defined. It does not snap. It does not break. It just skips the steps where the property is absent and interpolates across the rest.

How Do CSS Keyframes Connect to the Animation Property

YouTube player

A @keyframes block on its own does nothing. The animation property (or its sub-properties) is what activates a keyframe sequence on an element.

The connection is the name. animation-name references the exact identifier used in the @keyframes declaration. The match is case-sensitive. A mismatch means no animation runs, with no error thrown.

Sub-propertyWhat it controlsDefault value
animation-nameWhich @keyframes block to usenone
animation-durationHow long one cycle takes0s
animation-timing-functionSpeed curve between keyframe stepsease
animation-delayWait before the first cycle starts0s
animation-iteration-countHow many times the sequence repeats1
animation-fill-modeElement state before and after animationnone

All six sub-properties can be written as a single animation shorthand. The only ordering rule: animation-delay must come after animation-duration, since both accept the same value type (seconds or milliseconds).

One element can run multiple keyframe animations simultaneously. Separate each set of values with a comma in the shorthand, and each animation runs independently with its own timing.

What CSS Properties Can Be Animated with Keyframes

YouTube player

Not every CSS property responds to keyframe animation. Animatable properties are those with numeric or color values the browser can interpolate between. Non-animatable properties either snap instantly or get ignored.

GPU-Accelerated vs. Layout-Triggering Properties

This is the single most important performance distinction in CSS animation.

Compositor-only (GPU-accelerated): transform and opacity. These skip the layout and paint stages of the rendering pipeline entirely. The browser handles them on a separate compositor thread, off the main thread.

Paint-triggering: color, background-color, border-radius, box-shadow. These skip layout but still require the browser to repaint pixels. Moderately expensive.

Layout-triggering: width, height, margin, padding, top, left. These force a full reflow. Animating them can cause other elements on the page to shift, making every frame expensive to render.

SitePoint performance testing shows that animating margin-left via @keyframes produces an average of 44.82 fps, while the same movement using transform: translate3d() delivers 56.83 fps with zero layout or paint operations logged.

Discrete Properties

display and visibility are discrete. They do not interpolate. They snap from one value to another.

As of 2024, Chrome and Firefox added support for animating to and from display: none inside a @keyframes block, which removes the long-standing requirement to toggle visibility through JavaScript class manipulation (Chrome for Developers, 2024).

How Does animation-timing-function Affect Keyframe Playback

YouTube player

The animation-timing-function controls the speed curve between keyframe steps, not across the whole animation. This is an important distinction. Each segment between two keyframe selectors gets its own easing curve.

Built-in Easing Values

Five predefined keywords cover most use cases:

  • ease — starts slow, accelerates, ends slow (default)
  • ease-in — starts slow, ends fast
  • ease-out — starts fast, ends slow (feels natural for exits)
  • ease-in-out — symmetric slow-fast-slow curve
  • linear — constant speed throughout, best for spinners

For custom curves, cubic-bezier(x1, y1, x2, y2) accepts four values that define the control points of a Bezier curve. Tools like cubic-bezier.com let you preview and adjust these visually.

How steps() Creates Discrete Frame Animation

steps(n, direction) divides the animation into n equal discrete jumps instead of smooth interpolation. This is how CSS sprite sheet animations work.

A sprite sheet with 8 frames animating a running character would use steps(8). The browser jumps instantly between each frame rather than blending them, producing the illusion of traditional frame-by-frame animation.

The optional second argument (start or end) controls whether the jump happens at the beginning or end of each step interval.

What Is animation-fill-mode and How Does It Interact with Keyframes

YouTube player

animation-fill-mode is probably the most misunderstood animation property. By default, when a keyframe animation ends, the element snaps back to its pre-animation styles. Fill mode changes that behavior.

ValueBefore animation (during delay)After animation ends
noneOriginal stylesSnaps back to original
forwardsOriginal stylesRetains final keyframe state
backwardsApplies first keyframe stateSnaps back to original
bothApplies first keyframe stateRetains final keyframe state

The most common production use case for forwards: a fade-in animation that sets opacity from 0 to 1. Without animation-fill-mode: forwards, the element turns invisible again the moment the animation ends.

The most common use case for backwards: an element with a positive animation-delay that would otherwise flash at full opacity before the animation starts. Setting backwards applies the 0% keyframe state during the delay period.

How Do Multiple Keyframe Animations Run on One Element

A single element can run multiple @keyframes sequences simultaneously. Each animation is independent, with its own duration, delay, and timing function.

The syntax is comma-separated values in the animation shorthand:

` .card { animation: fade-in 0.4s ease-out forwards, slide-up 0.4s ease-out forwards; } `

Each comma-separated block maps to a separate @keyframes identifier. The animations run in parallel by default.

Property Conflicts Between Animations

Conflicts happen when two animations target the same CSS property on the same element. The browser resolves this by giving priority to whichever animation appears last in the declaration order (CSS-Tricks).

Practically, this means if fade-in and slide-up both animate opacity, the second one in the list wins. The most reliable way to avoid this is to keep each animation targeting distinct properties.

Staggering Multiple Animations

Staggering uses different animation-delay values to fire the same keyframe sequence at different times across multiple elements.

A common pattern in list animations applies the same @keyframes block to each list item, with incrementally larger delays (0s, 0.1s, 0.2s). The keyframe block is written once. The delay is what creates the cascade effect.

Tokopedia used this approach when building their scroll-driven product navigation bar, reducing CPU usage from 50% to 2% compared to their previous JavaScript scroll event implementation (Chrome for Developers, 2024).

How to Control CSS Keyframe Animations with JavaScript

YouTube player

CSS handles the animation sequence. JavaScript handles the when and whether. These two roles work best when kept separate.

The most common approach is adding and removing CSS classes to trigger or stop a keyframe animation, rather than writing animation properties inline via JavaScript.

Animation Events for Sequencing

The browser fires 4 animation events that JavaScript can listen for: animationstart, animationend, animationiteration, and animationcancel (MDN Web Docs).

The most useful in production is animationend. It fires when a keyframe animation completes one full run. From there you can chain a second animation, remove an element from the DOM, or swap a CSS class.

` element.addEventListener(‘animationend’, () => { element.classList.remove(‘slide-in’); }); `

One catch: if the element is removed from the DOM before the animation finishes, animationend does not fire. Plan removal timing around this.

Inline Style Overrides vs. Class Toggling

You can set element.style.animationPlayState = ‘paused’ to freeze a running keyframe animation mid-sequence without removing it entirely.

Class toggling is cleaner for most cases. Inline style overrides work when you need real-time control, like pausing on scroll or responding to user input mid-animation.

getComputedStyle(element).animationName returns the currently active keyframe name. Useful for checking which animation is running before modifying it.

Web Animations API as a Programmatic Alternative

The Web Animations API (element.animate()) gives JavaScript direct access to the browser’s animation engine. It takes the same keyframe structure as CSS @keyframes, but defined in JavaScript objects (MDN Web Docs).

Use CSS @keyframes for: animations that are always the same, triggered by class or state change.

Use the Web Animations API for: animations where duration, keyframe values, or timing depend on runtime data, like dynamically computed positions or user-generated values.

According to bitsofcode research, CSS @keyframes suit simple UI-level animations. The Web Animations API is better suited for fine-tuned control where values cannot be known at stylesheet write time.

What Are Common CSS Keyframe Animation Mistakes

Most animation bugs are not syntax errors. The keyframe block is written correctly, the animation runs, and the result just looks wrong or performs badly.

MDN performance documentation notes that 60 frames per second requires the browser to complete all rendering work within 16.7 milliseconds per frame. Animating the wrong properties makes that deadline impossible.

Animating Layout-Triggering Properties

Animating width, height, margin, padding, top, or left forces a full reflow on every frame. The browser recalculates the layout of the entire page, not just the animated element.

SitePoint performance tests show animating margin-left yields an average of 44.82 fps, versus 56.83 fps with transform: translate3d() under identical conditions. The difference shows in practice on low-end devices and mobile hardware.

The fix is almost always the same: replace positional properties with transform equivalents.

AvoidUse insteadWhy
left / toptransform: translate()Compositor-only, avoids reflow
width / heighttransform: scale()Skips layout and paint stages
background-coloropacity + overlayReduces repaint work on every frame

Missing animation-fill-mode on One-Shot Animations

An element fades in from opacity: 0 to opacity: 1. The animation ends. The element snaps invisible again.

This is the most reported CSS animation confusion in developer forums. The fix is one property: animation-fill-mode: forwards.

Without it, the element reverts to its pre-animation CSS state once the sequence ends. That’s the default. It behaves exactly as documented. It just surprises almost everyone the first time.

animation-name Case Mismatch

The identifier in animation-name must match the @keyframes name exactly, including capitalisation. fadeIn and fadein are two different names.

No error is thrown when the name does not match. The element simply does not animate. This is particularly tricky because the rest of the animation shorthand parses without issue.

Running transition and animation on the Same Property

Setting both transition: opacity 0.3s and an @keyframes block targeting opacity on the same element creates a conflict. The animation property wins, but the interaction between the two produces unpredictable results on state changes (CSS-Tricks).

Pick one per property. Transitions for two-state changes triggered by class or pseudo-class. Keyframe animations for everything multi-step or looping.

How Do CSS Keyframes Perform Across Devices

An animation that runs at 60 fps on a desktop with a discrete GPU can drop below 30 fps on mid-range Android hardware. The keyframe sequence is identical. The rendering pipeline is not.

Animation jank occurs when the browser cannot complete layout, paint, and composite operations within the 16.7ms frame budget (MDN Web Docs). On mobile CPUs, that budget runs out faster.

GPU Compositing with transform and opacity

The browser automatically promotes elements to their own compositing layer when a CSS keyframe animation targets transform or opacity. This offloads work to the GPU compositor thread, bypassing the main thread entirely (Viget).

The result: the animation continues smoothly even when the main thread is busy handling JavaScript, user input, or other layout work.

Tokopedia’s implementation of scroll-driven CSS animations replaced JavaScript scroll observers and reduced average CPU usage from 50% to 2% during scroll, without changing what users see (Chrome for Developers, 2024).

How will-change Affects Compositing

will-change: transform signals the browser to promote an element to a GPU compositing layer before the animation starts, eliminating the brief setup cost at animation begin.

Used correctly: on elements that animate frequently and consistently.

Used incorrectly: applied to every element on the page, consuming GPU memory for layers that rarely animate.

Viget’s animation performance research confirms that will-change consumes memory and resources per promoted layer. Over-promoting elements can outweigh the compositing benefit, especially on mobile with constrained memory.

prefers-reduced-motion for Accessibility Compliance

Vestibular disorders affect more than 70 million people, according to CSS-Tricks research referencing accessibility data. For those users, animations that spin, pulse, slide, or scroll can cause dizziness, nausea, and migraines.

The prefers-reduced-motion media query detects the user’s OS-level setting and lets you respond to it in CSS:

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

WCAG 2.3.3 (AAA) requires nonessential motion animations triggered by user interaction to be disableable. The prefers-reduced-motion query is the standard implementation path (W3C WAI).

The pattern works across Windows 10+, macOS, iOS, and Android 9+, all of which expose a system-level reduce-motion preference that the browser passes through to CSS (MDN Web Docs).

Testing Animation Performance in Chrome DevTools

Performance panel: records a timeline of frame rendering, showing where layout, paint, and compositing operations happen during animation playback.

Purple bars in the waterfall indicate layout recalculations. Green bars indicate paint operations. Both appearing during an animation cycle signal that the wrong properties are being animated (SitePoint).

The Layers panel shows which elements have been promoted to GPU compositing layers. An element animated with transform or opacity shows an orange border in the Layer Borders view during animation, confirming compositor-thread handling.

What Is the Difference Between CSS Keyframes and CSS Transitions

YouTube player

Transitions and keyframe animations both produce smooth property changes. They solve different problems and should not be used interchangeably.

A transition reacts. It fires when a CSS property changes value, like on hover or class toggle. You cannot start a transition without a triggering state change.

A keyframe animation acts. It runs on its own timeline, independent of state changes. It can start on page load, loop indefinitely, or be triggered by a class without a corresponding reversal.

FeatureCSS TransitionCSS @keyframes
TriggerProperty value changeanimation property applied
Steps2 states only (start + end)Unlimited percentage steps
LoopingNot supportedanimation-iteration-count: infinite
Auto-startNoYes, on element load
JavaScript eventstransitionendanimationstart, animationend, animationiteration

Transitions are the right tool for CSS hover effects, focus states, and two-state UI changes. Keyframe animations handle loaders, entrance animations, micro-interactions, and anything requiring intermediate steps or looping motion.

For animating SVG with CSS, keyframes handle path-based and multi-property sequences that transitions cannot express in a single declaration.

The Loopaloo animation guide (2024) summarises the decision rule cleanly: use transitions for two-state changes, keyframes for multi-step sequences, and the Web Animations API when JavaScript needs control at compositor-thread performance.

FAQ on CSS Keyframes

What are CSS keyframes?

CSS keyframes define the intermediate states of an animation sequence using the @keyframes rule. Each step maps a percentage of the animation timeline to specific CSS property values. The browser interpolates all frames in between automatically.

What is the difference between @keyframes and CSS transitions?

Transitions animate between 2 states and need a trigger. Keyframe animations run on their own timeline, support unlimited steps, loop indefinitely, and start without any state change required.

Do CSS keyframes work in all browsers?

Yes. The @keyframes at-rule has full support in Chrome 43+, Firefox 16+, and Safari 9+. All modern browsers support the complete CSS animation property set without vendor prefixes.

Which CSS properties can be animated with keyframes?

Any property with interpolatable values: transform, opacity, color, background-color, border-radius. Discrete properties like display snap instantly. For best performance, stick to transform and opacity.

What does animation-fill-mode do in a keyframe animation?

animation-fill-mode: forwards keeps the element in its final keyframe state after the animation ends. Without it, the element snaps back to its original styles. Use both to apply start and end states.

How do you loop a CSS keyframe animation?

Set animation-iteration-count: infinite. Pair with animation-direction: alternate to reverse playback on each cycle. Use linear as the timing function for spinners and continuous motion loops.

Can you control CSS keyframe animations with JavaScript?

Yes. Toggle classes to trigger animations, listen for animationend to chain sequences, and use animation-play-state to pause mid-sequence. The Web Animations API gives full programmatic control via element.animate().

Why is my CSS keyframe animation not working?

The most common causes: missing animation-duration (defaults to 0s), a case mismatch between animation-name and the @keyframes identifier, or a conflicting transition on the same property.

How do you make CSS keyframe animations accessible?

Use the prefers-reduced-motion media query to disable or simplify animations for users who have enabled reduced motion in their OS settings. Vestibular disorders affect over 70 million people, making this a real accessibility concern.

What is the fastest way to animate with CSS keyframes?

Animate only transform and opacity. These properties run on the GPU compositor thread, bypassing layout and paint entirely. Avoid width, height, and positional properties, which trigger reflow on every frame.

Conclusion

This conclusion is for an article presenting how the @keyframes at-rule gives you precise control over every step in a CSS animation sequence.

From percentage selectors and animation timing functions to fill modes and GPU compositing, the mechanics are consistent once you understand them.

Stick to transform and opacity for smooth, compositor-thread performance. Avoid layout-triggering properties and always account for animation-fill-mode on one-shot sequences.

Respect the prefers-reduced-motion media query. Animation playback control matters beyond aesthetics, it directly affects web accessibility for millions of users.

The tools covered here, from the steps()` function to the Web Animations API, cover the full range of what CSS keyframe animation can do in production.

Author

Bogdan Sandu specializes in web and graphic design, focusing on creating user-friendly websites, innovative UI kits, and unique fonts.Many of his resources are available on various design marketplaces. Over the years, he's worked with a range of clients and contributed to design publications like Designmodo, WebDesignerDepot, and Speckyboy, Slider Revolution among others.