CSS & Web Design

CSS Keyframe Animation Guide: Stops, Timing and Easing in Practice

By Smart Tools Utility Team · Published October 7, 2026 · Updated October 7, 2026 · 8 min read

A CSS animation has two halves: a @keyframes block that says what the element looks like at certain moments, and an animation declaration that says how long it takes and how it plays. Most confusion comes from mixing the two up. The CSS Animation / Keyframes Generator writes both halves for you from a small visual timeline. This guide shows how to use it, but its main goal is to explain what the generated numbers do. We sampled the real animations in a browser at specific moments so the figures below are measurements, not guesses.

Keyframes and the animation rule

The tool always produces two pieces of CSS. This is the default Bounce preset:

@keyframes customAnimation {
  0% { transform: translateY(-0px); }
  50% { transform: translateY(-30px); }
  100% { transform: translateY(-0px); }
}

.animated-element {
  animation: customAnimation 1s ease infinite normal;
}

The @keyframes block defines the named animation customAnimation. The class .animated-element attaches it and sets the shorthand in this order: name, duration, timing function, iteration count, direction. Nothing happens until some element has that class. The names are fixed in the tool, so rename both customAnimation and .animated-element to something meaningful in your project, and keep them matching. If you paste two outputs into one stylesheet without renaming, the second @keyframes customAnimation will override the first.

Using the generator

  1. Pick an Animation Type. The four presets are Bounce, Fade, Pulse and Rotate. Each preset also decides which CSS property the timeline controls (next section).
  2. Shape the Timeline. Every stop card has a Position slider (0 to 100%) and a value slider. Add Stop inserts a new stop at 50% with a value of 50, and the small x button on a card removes it. The tool never lets you go below two stops.
  3. Set Duration (0.2s to 5s), Iteration (Infinite, 1, 2 or 3), Timing Function (Ease, Linear, Ease In, Ease Out, Ease In Out) and Direction (Normal, Alternate, Reverse).
  4. Watch the Live Preview. It restarts whenever you change something, and Replay restarts it on demand, which is handy for finite iteration counts.
  5. Click Copy CSS and paste it into your stylesheet.
The Animation Generator on the Bounce preset with four timeline stops and the generated keyframes listing 0%, 25%, 50% and 100%
A fourth stop added at 25% with a height of 60. The stops are written to the CSS in order of position, not in the order they were created.

What each preset really changes

Each preset animates exactly one property, and the value slider is converted like this:

Two consequences follow. First, switching presets resets the timeline to that preset's defaults, so choose the preset before you fine-tune the stops. Second, you cannot combine effects, such as a fade-in with a bounce, in one generated animation. Three of the four presets use transform, and a property can only be animated by one animation at a time on an element. We tested putting a bounce class and a rotate class on the same element: only the later declared animation (the rotate) was applied. To combine effects, nest them, with the bounce on a wrapper element and the rotation on the child, or hand-write one @keyframes that sets transform and opacity together.

How timing functions actually apply

This is the part that surprises people. The timing function is applied between each pair of neighbouring keyframes, not across the whole animation. In a three-stop Bounce, the easing runs once for the climb from 0% to 50% and again for the fall from 50% to 100%. We paused the default Bounce at several moments with ease: at 0.25s the box was at -24.07px, at 0.5s at -30px, and at 0.75s at -5.93px. With linear easing the same moments would show -15px, -30px and -15px. Ease picks up speed quickly and then spends a long time slowing down, so each leg of the movement settles gently into its stop.

That is why Rotate with the default Ease feels jerky when looped. We measured a one-second infinite Rotate with ease: the angle was 147 degrees at 0.25s, 288 degrees at 0.5s and 346 degrees at 0.75s. The box covers 40 percent of the turn in the first quarter of the time, then crawls through the last stretch before the loop restarts. With linear it was exactly 90, 180 and 270 degrees. For anything continuous, such as a spinner, choose Linear. For one-shot motion like a fade-in, Ease Out usually feels natural because it decelerates into place.

The Animation Generator on the Pulse preset with Ease In Out and Alternate selected and the generated CSS showing scale values 1.00, 1.30 and 1.00
Pulse with a 1.6s duration, Ease In Out and Alternate. The scale stops are shown as percentages in the sliders and as decimals in the CSS.

Ready to build a keyframe animation?

Open the Animation Generator

Direction, iterations and what happens at the end

Normal plays 0% to 100% each time. Reverse plays 100% to 0%, which means a Fade with Reverse and Iteration 1 is a fade-out. Alternate plays forward, then backward, then forward again. It is useful for animations whose end does not match their start, such as a Fade with two stops (0 then 100) looping infinitely: Normal pops back to invisible at every restart, while Alternate fades in and out smoothly. For the symmetrical Bounce and Pulse presets, Alternate looks the same as Normal, because the animation is already a mirror image of itself.

With a finite Iteration count, the element jumps back to its normal style when the animation ends. We tested a fade-out that runs once: when it finished, the element's opacity returned to 1 and it reappeared. The generated CSS does not include animation-fill-mode. Adding forwards to the shorthand (for example animation: customAnimation 1s ease 1 reverse forwards) kept the element at the final keyframe in our test. Similarly, if you add a delay for a Fade-in, add backwards so the element stays invisible during the wait; in our test, a Fade with a 0.5s delay showed at full opacity before it started.

Working with timeline stops

Respect reduced motion

Large or endless motion can be uncomfortable for some visitors, and many people enable a system setting asking for less movement. The generated CSS does not account for it, so add this after your animation rule:

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

We checked this in a browser with the reduced-motion preference emulated: the element's animation name became none, and without the preference it stayed customAnimation. Reserve infinite animations for loading indicators or small decorative touches, avoid them near body text, and never rely on an animation alone to convey important information.

Performance and when to use something else

The properties this tool outputs are transform and opacity. Browsers can usually animate those without recalculating the page layout, so they are the safest choices for smooth motion, and animating width, height, top or left is more likely to cause jank. CSS keyframes are well suited to simple loops, entrance effects and attention cues. If you need motion that reacts to scrolling, chains many steps with exact coordination, or must be controlled from code (pause, seek, reverse mid-way), a JavaScript animation approach is more practical. For a plain state change such as a hover, a transition is simpler than keyframes.

The tool works in your browser. We watched the network during editing and saw no requests carrying your settings, and nothing is stored between visits. The Contact tab's feedback form is the only thing that sends data.

Known cosmetic issue: when a preset has three or more stops, the remove (x) button sits on top of the percentage label in each stop card, which makes the small text harder to read. It does not affect the generated CSS.

Quick answers

Can I use cubic-bezier or steps timing? Not in the tool, which offers five keywords. Edit the shorthand by hand, for example cubic-bezier(0.34, 1.56, 0.64, 1), after copying.

How do I animate on hover or when an element scrolls into view? Put the animation declaration on :hover, or add the class with a small script when the element appears. The generator only produces the animation itself.

Why does my animation snap back at the end? That is the default for finite iterations. Add forwards as the fill mode.

Is the preview's pink square my element? No, it is only a demo box. The CSS applies to whichever element you give the class to.

Ready to build a keyframe animation?

Open the Animation Generator