Free @keyframes authoring tool
Keyframes generator for stops you choose yourself
This keyframes generator is free and needs no signup: every row in the table is one stop in an @keyframes block, and you decide what percentage it sits at, what translate, scale, rotate and opacity it holds, and which of seven timing functions governs the interval leaving it. Drag the timeline slider and the preview walks the stop list the way the engine does, one interval at a time, so you can see which pair of stops a given frame came from. Twelve stops is the ceiling, duplicate percentages and missing endpoints are called out above the table, and the at-rule is on your clipboard in one press.
- 100% free
- No signup
- Up to 12 stops
- Easing per interval
- Scrub the timeline
Stops (3 of 12)
| At % | X px | Y px | Scale | Rotate | Opacity | Easing out of this stop | Remove |
|---|---|---|---|---|---|---|---|
The rule
@keyframes rise-in {
0% {
opacity: 0;
transform: translate(0px, 28px) scale(0.92) rotate(0deg);
animation-timing-function: ease-out;
}
65% {
opacity: 1;
transform: translate(0px, -8px) scale(1.04) rotate(0deg);
animation-timing-function: ease-in-out;
}
100% {
opacity: 1;
transform: translate(0px, 0px) scale(1) rotate(0deg);
}
}16 lines, 373 bytes. Every stop carries the same four transform functions in the same order, because a browser only interpolates two transform lists when they match position for position.
How to write an @keyframes block
From an empty timeline to a pasteable at-rule in three passes.
Put the stops where the motion changes
Start from the three-stop block the tool opens with and press Add a stop to split the widest gap in the timeline. The percentage field takes anything from 0 to 100 in tenths, so a stop at 62.5% is as legal as one at 60% — percentages are positions on the timeline, not a grid you have to snap to. Twelve stops is the ceiling here, and a block that needs more than about eight is usually two animations sharing one name.
Set the five values, then the easing that leaves the stop
Each row carries translate X, translate Y, scale, rotate and opacity, and the last column is the timing function for the interval that begins at that stop rather than for the whole animation. Set 0% to ease-out and 65% to ease-in-out and the block accelerates out of the start and settles into the finish, which is a thing no single animation-level curve can do. The last stop's easing is written out but never used, because no interval starts there.
Scrub it, then take the at-rule
Play the rule hands the block to the browser and loops it; Scrub by hand freezes the box and lets you drag the timeline to any position, interpolating the two stops on either side exactly as the engine would. Scrub is the mode that shows you which interval a value came from — and it is the mode the tool starts in if your system asks for reduced motion. Copy the @keyframes block takes the at-rule alone, with its line and byte count printed underneath.
Technical specifications
| Stops | 2 minimum, 12 maximum, anywhere from 0% to 100% at a resolution of 0.1% — Add a stop splits the widest gap on the timeline |
|---|---|
| Values per stop | 6: translate X, translate Y, scale, rotate, opacity, and the timing function for the interval that starts there |
| Per-interval easing | 7 choices — linear, ease, ease-in, ease-out, ease-in-out, step-start and step-end |
| Preview modes | 2: the browser running the generated at-rule on a loop, or a hand scrub in 0.5% increments that interpolates the surrounding pair of stops |
| Transform output | translate() scale() rotate() emitted on every stop in that fixed order, which is the condition for component-wise interpolation |
| Name handling | Characters outside A-Z, a-z, 0-9, hyphen and underscore are stripped, a leading digit is removed, and an empty field falls back to custom-move |
| Warnings raised | Duplicate percentages, a missing 0% and a missing 100% — the three cases where valid CSS does something other than what was intended |
| Output | The @keyframes at-rule on its own with a live line and byte count; free, no signup, and nothing about the block is sent anywhere |
Frequently asked questions
Do I have to write a 0% and a 100% keyframe?
No, and what happens when you leave one out is worth understanding rather than avoiding. CSS synthesises the missing endpoint from the element's own computed style, so a block that starts at 20% will animate from whatever the element already looks like up to your first stop. That is genuinely useful for an entrance that should begin wherever the element happens to be, and it is a trap when you assumed the animation would start at your first written stop — which is why this tool says so out loud when either endpoint is absent.
Can two stops sit at the same percentage?
Yes, it is legal, and the one written later in the block wins for any property they both declare. The keyframe list follows document order rather than specificity, so two rules at 50% do not merge into an average or throw an error — the second silently replaces the first, property by property, and any property only the first one set survives. Duplicate percentages are flagged above the table because the failure is invisible in the output.
Why does my transform jump between two stops instead of gliding?
Because the two transform lists do not match function for function. A browser interpolates translate(0px) scale(1) against translate(20px) scale(1.2) component by component, but it cannot pair scale(1.2) with rotate(45deg), so it falls back to a matrix interpolation or, where the lists differ in length or order, to a discrete swap at the halfway point. That is why every stop this tool writes carries translate, scale and rotate in that same order even when two of them are at their identity values.
Does animation-timing-function belong in the keyframe or on the animation?
Both are valid and they mean different things. Declared on the animation it is the default curve for every interval; declared inside a keyframe it overrides that default for the one interval that starts at that keyframe and runs to the next. Putting it on the final 100% stop does nothing at all, since no interval begins there — a line of CSS that survives review because it looks correct.
Is !important allowed inside @keyframes?
No — a declaration marked !important inside a keyframe is dropped entirely, and the specification is explicit about it. This surprises people who reach for it to beat an inline style, and there is no workaround inside the at-rule: the way to raise an animation's weight is on the rule that sets animation-name, or by animating a custom property that the inline style reads.
Which properties can go in a keyframes block?
Only animatable ones, and the useful set is narrower than the legal set. transform and opacity are handled by the compositor and cost nothing per frame; width, height, top and margin are animatable but force layout on every frame; display and visibility are discrete, flipping at the halfway point unless you opt into transition-behavior: allow-discrete. Anything the browser cannot interpolate is simply skipped, with no warning in the console.
Why is the browser ignoring my keyframes block entirely?
Nine times out of ten the name in the at-rule and the name in animation-name disagree, and keyframes names are case-sensitive: fadeIn and fadein are two different animations, and referencing a name that was never defined is not an error, it is an animation with no frames. The other common cause is a name that is a CSS-wide keyword — none, initial, inherit, unset and revert cannot be used, and the tool's name field will not let you type a leading digit either.
About the @keyframes at-rule
An @keyframes block is a sparse description of a timeline, and every sentence of trouble people have with it comes from forgetting the word sparse. You are not listing frames; you are listing the handful of instants at which you have an opinion, and the engine fills everything between them by interpolating the two stops on either side. That is why a percentage is a position and not an index — a stop at 62.5% is as ordinary as one at 60% — and it is also why a property that appears at 0% and 100% but not at the three stops in between glides straight through them, untouched by the stops it passes.
The rule most worth internalising is the one about timing functions, because it is where the at-rule stops behaving like the shorthand people learn first. Write animation-timing-function inside a keyframe and it applies to the interval that starts at that keyframe, not to the animation. A five-stop block therefore has four independent curves in it, and the default is that all four are the animation's curve, which is almost never what a designer means: setting ease-in-out on an animation with five stops makes it accelerate and decelerate four separate times, producing the stutter that gets blamed on the browser. Setting a single curve for the whole run is what the shorthand on the CSS animation generator is for; shaping each leg separately is what this page is for, and the curves themselves come from the cubic bezier generator.
Two practical constraints shape what belongs in a block. Interpolation between transform lists is component-wise only when the lists match function for function, so a stop holding scale() next to one holding rotate() falls back to matrix maths and often to a visible snap — this tool writes all three functions on every stop for that reason. And animating anything that changes an element's box makes the browser lay the page out again on every frame, so a block that grows a card is far cheaper written as a scale over a box whose size is already reserved, which is a job for the aspect ratio calculator. If your stops carry color rather than geometry, choose the endpoints deliberately: two hues that are neighbors will read as a flicker rather than a transition, which is what the palette generator and the complementary color page are for.
Where the at-rule is assembled
Every number on this page is worked out by JavaScript running in the tab you are reading it in. Nothing you type, paste or open is uploaded, logged or kept, which is also why the tools carry on working after you disconnect from the network.
The block itself is put together by string concatenation inside this page and handed to the clipboard by the browser's own clipboard API. There is no copy endpoint on this site, so nothing about your stop list — its percentages, its values or its name — is ever visible to anything but your own browser.