ScreenshotNeo

BlogGuides

CSS Keyframe Animations: A Practical Guide

Learn how CSS @keyframes work, configure animations, handle reduced motion, and avoid common timing and performance pitfalls.

By the ScreenshotNeo team4 October 20269 min read

CSS keyframe animations let you describe styles at points along a timeline, then tell an element how and when to run that sequence. Define a named @keyframes rule, connect it with animation-name or the animation shorthand, and choose timing, repetition, direction, and fill behavior.

For example, this makes a card fade in while moving upward:

.card {
  animation: 600ms ease-out 1 both enter;
}

@keyframes enter {
  from {
    opacity: 0;
    transform: translateY(0.75rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

The animation properties configure the run; the keyframes define the styles through which it moves. The browser interpolates between frames for properties that support interpolation. See MDN’s guides to using CSS animations and the @keyframes at-rule.

1. Create a CSS keyframe animation

  1. Choose the element and the visual change it needs.
  2. Write a uniquely named @keyframes rule describing the start, end, and any important intermediate styles.
  3. Apply the animation name and its timing settings to the element.
  4. Check the result in the page, including reduced-motion settings and the final state after the animation ends.

Here is a complete HTML file you can save as keyframes.html and open in a browser. It includes a button that starts the animation when clicked:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CSS keyframe example</title>
  <style>
    .card {
      width: min(20rem, 100%);
      padding: 1.5rem;
      border: 1px solid #d5d9e0;
      border-radius: 0.75rem;
      background: white;
      color: #172033;
    }

    .card.is-entering {
      animation: 600ms ease-out 1 both enter;
    }

    @keyframes enter {
      from {
        opacity: 0;
        transform: translateY(0.75rem);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .card.is-entering {
        animation: none;
      }
    }
  </style>
</head>
<body>
  <button id="show" type="button">Show card</button>
  <article class="card" id="card">
    <h1>A small animation</h1>
    <p>The card enters with a fade and a short upward movement.</p>
  </article>
  <script>
    const button = document.querySelector('#show');
    const card = document.querySelector('#card');

    button.addEventListener('click', () => {
      card.classList.remove('is-entering');
      // Read layout so a second click can restart the CSS animation.
      void card.offsetWidth;
      card.classList.add('is-entering');
    });
  </script>
</body>
</html>

The class controls when the animation runs. In an application, you can add it on page load, in response to an interaction, or when an element enters view. If the effect is only a simple change between two states, a CSS transition may be easier to maintain; keyframes are useful when the sequence needs intermediate waypoints, repetition, or direction control.

2. Write the @keyframes timeline

A keyframe selector marks a position in the animation’s active duration. from is an alias for 0%, and to means 100%. Percentages are timeline positions, not seconds: 50% is halfway through the active duration.

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

The selectors may appear in any order in the rule; the browser evaluates them in timeline order. A single keyframe can set several properties. For example, opacity and transform can both change at the same intermediate point.

Missing start or end frames

You do not have to specify both endpoints. If a 0% or 100% frame is absent, the element’s computed style supplies the missing endpoint. This can be useful when an animation should begin from the element’s current style or return to its original style. It can also produce a surprise if the element’s computed style changes because another rule or class applies.

Properties and interpolation

Properties animate only when CSS supports the required interpolation. A property omitted from one frame may still animate between the defined value and the computed style, when interpolation is supported. Non-interpolable properties are dropped from that animation. Unsupported or non-animatable declarations do not prevent other supported properties in the same keyframes from animating.

Keep the keyframe name unique across the stylesheets that apply to the page. If the same name is declared in multiple @keyframes rules, those rules do not merge: the last encountered matching rule is used. Accidental duplicate names can make an otherwise valid animation appear to use the wrong sequence.

3. Configure animation timing and behavior

The animation shorthand combines several settings into one declaration. The example 600ms ease-out 1 both enter specifies a duration, easing function, iteration count, fill mode, and name. For clarity, especially when an animation is maintained by a team, the longhand properties can be written separately:

.card {
  animation-name: enter;
  animation-duration: 600ms;
  animation-timing-function: ease-out;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: both;
  animation-play-state: running;
}
Setting What it controls Common values
animation-name Which named keyframes to run. A keyframe name; none disables the named animation.
animation-duration Length of one active iteration. A time such as 300ms or 1s.
animation-timing-function How progress is paced between keyframes. ease, linear, ease-in, ease-out, ease-in-out, or a custom curve.
animation-delay Wait before the animation starts. A time such as 0s or 150ms; a negative delay begins partway through the sequence.
animation-iteration-count How many times to run. A number such as 1 or 3, or infinite.
animation-direction Whether alternate iterations reverse direction. normal, reverse, alternate, alternate-reverse.
animation-fill-mode Whether keyframe styles apply outside the active run. none, forwards, backwards, both.
animation-play-state Whether the animation is running or paused. running, paused.

Duration and delay

A missing animation-duration defaults to 0s. The animation can still produce animation events even though no visible interval is rendered. Set a nonzero duration when you expect to see movement. A delay postpones the active run; a negative delay starts as though part of the duration has already elapsed.

Iteration and direction

Use a finite iteration count for a one-time entrance or feedback effect. Use infinite only for an effect that should continuously repeat, such as a subtle loading indicator. With alternate, every other iteration runs in reverse, which can avoid an abrupt jump back to the first frame. Do not use continuous movement when a static or user-controlled indicator would communicate the same state.

Fill mode and final state

With animation-fill-mode: none, keyframe styles do not remain applied after the active run. forwards keeps the last keyframe’s styles after it finishes; backwards applies the first relevant frame during the delay; both combines those behaviors. Use a fill mode when the visual state should persist, and make sure the underlying styles are sensible if the animation class is later removed.

Multiple animations

You can apply more than one animation by listing comma-separated names and values. Keep each position in the lists aligned: the first duration and timing function configure the first animation, and so on.

.notice {
  animation-name: enter, glow;
  animation-duration: 400ms, 1.2s;
  animation-timing-function: ease-out, ease-in-out;
  animation-iteration-count: 1, 2;
  animation-fill-mode: both, none;
}

@keyframes enter {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes glow {
  0%, 100% { box-shadow: 0 0 0 transparent; }
  50% { box-shadow: 0 0 1rem #f2c94c; }
}

Use the shorthand for compact declarations, or longhands when a long list would be hard to map to its animations. When combining multiple animations, inspect the computed styles if two sequences affect the same property.

4. Respect reduced-motion preferences

Some kinds of movement, flashing, or blinking can cause difficulty for people with vestibular disorders, epilepsy, migraine, or other motion sensitivities. The prefers-reduced-motion media query lets a page respond to the user’s operating-system preference. MDN describes it as a way to provide an experience with fewer animations and transitions for users who have chosen reduced motion. Read MDN’s prefers-reduced-motion reference.

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

This is a starting point, not a universal rule to erase all motion. For nonessential decoration, remove or substantially reduce the effect. If motion carries information or is essential to a function, provide a clear alternative and consider a way to pause or disable it. Check that the static state still communicates the content and state.

5. Consider performance and rendering cost

CSS animations are not automatically inexpensive. The cost depends on the animated property, the element, and the rendering work the page triggers. Changes to box-model properties can cause layout recalculation and repaints. MDN’s animation performance guide explains how style recalculation, layout, and paint can contribute to jank.

transform is often a useful choice for movement, and opacity is often useful for fades, but neither is a blanket guarantee of smooth performance. Profile the actual page on the devices that matter when an animation is important or runs frequently. Avoid animating large areas or expensive visual effects without checking their impact, and prefer short, purposeful sequences over persistent motion when the interface does not need it.

6. Troubleshoot common problems

Symptom Likely cause Fix
Nothing appears to animate. The name does not match, the rule is missing, the selector does not apply, or the duration is zero. Check the computed animation-name, confirm the matching @keyframes is loaded, and set a nonzero duration.
The animation runs but starts or ends at an unexpected style. An endpoint is omitted, or another stylesheet/class changes the computed style. Declare explicit 0% and 100% frames if you need fixed endpoints; inspect computed styles and rule precedence.
The final keyframe disappears after completion. The default fill mode does not retain the final frame. Use forwards or both when the final visual state must persist, or set the resting style directly on the element.
A keyframe property does not change. The property is not animatable, the values cannot interpolate, or another rule overrides the result. Check that the property and values support interpolation, then inspect computed styles and the cascade.
The wrong sequence plays. Another @keyframes rule with the same name appears later in the stylesheet order. Rename the sequence or remove the duplicate; duplicate named rules replace rather than merge.
A class toggle does not restart the effect. The class stayed applied, so no new style change restarted the animation. Remove and re-add the class after the style has been removed, or use a state-driven animation pattern appropriate to the framework. The runnable example forces a layout read between those steps.
The animation feels choppy. The animated properties or page rendering work are costly for the target device. Try an appropriate transform-based effect, reduce expensive work, and profile the real page rather than assuming a property is always cheap.
Motion remains when a user prefers less. The reduced-motion query does not cover the selector or animation in question. Audit animated elements and add reduced-motion behavior for each nonessential effect.

7. Capture a page or animation state with ScreenshotNeo

If you need a screenshot of a page that uses an animation, use ScreenshotNeo, a website screenshot API and MCP server for developers. The API can return an image or PDF from a URL, and its capture options include waiting for a delay, a selector, or network idle. See the ScreenshotNeo API documentation.

Or skip the browser setup

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie and consent banners are accepted and removed before capture; newsletter popups and chat widgets are removed too. Each step can be turned off.
  • Bot checks, blank pages, timeouts, and failed loads are not billed. Response headers say the page verdict and whether the request was billed; cache hits also cost nothing.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

8. Quick checklist

  • Give each keyframe sequence a clear, unique name.
  • Set a nonzero duration when you expect a visible animation.
  • Use explicit endpoints when computed styles should not determine the start or finish.
  • Choose fill mode based on the state needed before and after the active run.
  • Provide reduced-motion behavior for nonessential movement.
  • Profile animations that affect important or frequently updated interface elements.

Frequently asked questions

Should I use a transition or keyframes?

Use a transition for a change between states when you do not need a timeline of intermediate waypoints. Use keyframes when the effect needs multiple stages, repetition, or direction control.

Does 50% mean half a second?

No. It means halfway through the active duration. For a one-second animation, the midpoint is half a second after the active run begins, excluding any delay.

Can an animation run only once?

Yes. Set animation-iteration-count: 1, which is also the usual default.

Why do animation events fire when I cannot see an animation?

A zero-second duration can still produce animation events while rendering no visible animation. Give it a nonzero duration if it should be seen.