@keyframes

Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.

The @keyframes at-rule sets the value of properties at different points during an animation, so instead of defining how each property should behave at each frame of an animation, we set its keyframes, and CSS will figure out (i.e., interpolate) the values between them.

You can think of the animation property with @keyframes as the big brother to the transition property, allowing control between steps of the animation sequence.

/* Define the animation */
@keyframes spin {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(720deg);
  }
}

/* Apply the animation */
img {
  animation: spin 1s infinite;
}

The @keyframes at-rule is defined in the CSS Animations Level 1 specification.

Syntax

@keyframes = @keyframes <keyframes-name> { <qualified-rule-list> }

<keyframes-name> = <custom-ident> | <string>

<keyframe-block> = <keyframe-selector># { <declaration-list> }

<keyframe-selector> = from | to | <percentage [0,100]>

Cool, but a more readable syntax would be:

@keyframes <keyframes-name> {
  <keyframe-selector> {
    <declaration-list>
  }

  <keyframe-selector> {
    <declaration-list>
  }
}

Arguments

/* `from` and `to` selectors */
@keyframes my-animation {
  from {
    transform: translate(-100px);
  }

  to {
    transform: translate(100px);
  }
}

/* Percentages form */
@keyframes my-animation {
  0% {
    transform: translate(100px);
  }

  50% {
    transform: translate(-100px);
  }

  100% {
    transform: translate(100px);
  }
}

/* We can combine percentages */
@keyframes my-animation {
  0%,
  100% {
    transform: translate(100px);
  }

  50% {
    transform: translate(-100px);
  }
}
  • <keyframes-name>: Either a <custom-ident> or <string> representing the name of the @keyframes. If a <custom-ident> is used, then it cannot match any CSS keyword like none or initial.
  • <keyframe-selector>: Specifies a point in the animation. It can be a percentage from 0% to 100%, so 33% is a third of the animation, 50% a half, and so on. You can also use the from and to selectors which equal 0% and 100%, respectively. Values outside the 0% to 100% range are ignored.
  • <declaration-list>: Written as normal CSS declarations, it sets the properties on each keyframe.

Basic usage

The simplest @keyframes at-rule only has a final keyframe, defined with the to or 100% selector. For example, the following @keyframes will animate from the element’s current width to 50%.

@keyframes resize {
  to {
    width: 50%;
  }
}

Then we can use it inside the animation property along the time of the animation:

.element {
  animation: resize 1.5s infinite alternate; /* infinite and alternate for demo  */
}

We can also set the initial keyframe, defined with the from or 0% selector:

@keyframes resize {
  from {
    width: 50%;
  }

  to {
    width: 25%;
  }
}

We can also add keyframes in between, so we could set a keyframe at half of the animation with 50%:

@keyframes resize {
  from {
    width: 50%;
  }

  50% {
    width: 75%;
  }

  to {
    width: 25%;
  }
}

And don’t forget! We can animate almost all properties inside a @keyframes:

@keyframes remake {
  from {
    width: 25%;
    border-radius: 60% 40% 70% 30% / 30% 30% 70% 70%;
    background-color: lch(77% 91 135);
  }

  to {
    width: 50%;
    border-radius: 60% 40% 33% 67% / 30% 45% 55% 70%;
    background: lch(77% 93 165);
  }
}

Using several @keyframes together

The last snippet was one @keyframe packing a lot of different properties, but it may be better to animate fewer properties in different @keyframes and then combine their effects on one animation property.

@keyframes resize {
  /* ... */
}

@keyframes reborder {
  /* ... */
}

@keyframes recolor {
  /* ... */
}

That way, we can create different animations depending on the element.

#first {
  animation: resize 1.5s infinite alternate, recolor 1.5s infinite alternate;
}

#second {
  animation: resize 1.5s infinite alternate, reborder 1.5s infinite alternate;
}

Common custom properties won’t work

As a recap, @keyframes defines the element’s styles at specific points throughout the animation, and the browser will figure out the values between them. If we give it an initial and final width, it will gradually increase/decrease the pixels used, or in the case of color, the browser will move through the corresponding color space to reach the final color. This process of the browser making out the in-betweens is called interpolation and works quite well in @keyframes with one exception: custom properties.

For example, if we try to animate a color saved in a custom property:

@keyframes repaint {
  from,
  to {
    --my-color: lch(77% 91 135);
  }

  50% {
    --my-color: lch(77% 93 165);
  }
}

.blob {
  background-color: var(--my-color);
  animation: repaint 2s infinite;
}

Notice the flex! We are using lch() instead of rgb() or hsl()!

It will abruptly change colors and forget to animate them. Well, it doesn’t actually forget, CSS simply can’t animate it. If we declare an inline custom property, CSS treats its content as a string rather than a number and we can’t interpolate that! Instead, we have to let CSS know that the custom property holds a color using the @property at-rule:

@property --my-color {
  syntax: "<color>";
  initial-value: lch(77% 91 135);
  inherits: true;
}

/* ... */

And now CSS can interpolate the values in between the color and give us an actual animation:

You can read more about this in “Interpolating Numeric CSS Variables”. And while we’re talking about animating colors, you might find it interesting that CSS uses color mixing when interpolating between color values.

Changing the animation in @keyframes

All animation functions are usually ignored inside @keyframes; it would be counter intuitive to change the animation behavior while it’s running and could also break it by changing its animation-name or animation-duration . However, there is one exception: the animation-timing-function.

The animation-timing-function property has a special interpretation inside @keyframes since it defines the timing function used for the next keyframes.

@keyframes resize {
  from {
    width: 100%;
    animation-timing-function: ease-out;
  }

  50% {
    width: 50%;
    animation-timing-function: ease-in;
  }

  to {
    width: 100%;
  }
}

.element {
  animation: resize 2.5s infinite;
}

Note: Using animation-timing-function on the 100% mark will be ignored since the animation will be finished at that point.

Some details about @keyframes

The @keyframes at-rule has a lot of details that, while not extremely important, are likely to be overlooked while using it. I did my best to gather some below and hopefully save someone a feature headache:

Names in @keyframes

Writing the name of @keyframe as a string or a custom ident doesn’t make any difference, so the following @keyframe rules are exactly the same, with the latter replacing the former:

@keyframes my-animation { /* ... */ }
@keyframes "my-animation" { /* ... */ }

Although uppercase and lowercase characters do make a difference since @keyframes is case-sensitive and matched codepoint-by-codepoint. For example, the following @keyframes rule is different from the last two:

@keyframes My-Animation { /* ... */ }

@keyframes is element agnostic

You’ll notice there isn’t any mention of elements when writing @keyframes. This is a design choice, since @keyframes rules are written to be reused on different elements. Then, it’s worth noticing that values and units relative to an element (like em%currentColor, etc.) don’t have an intrinsic value in @keyframes, but once used in an element’s animation property will reference that element.

Discrete properties in @keyframes

Each property in CSS has an “anim­ation type”, which sets how it behaves when we try to animate or transition it. There are three animation types: animatablediscrete, and not animatableanimatable properties are interpolable, i.e. if given a start and end value, CSS can make the values in between and animate, like width or color. However, discrete properties (like text-align or flex-direction) don’t have values in between, so if we try to animate them using @keyframes CSS will abruptly switch them at the middle of the animation.

There is one exception, the display and content-visibility properties which are both discrete, but if used inside @keyframes they will be ignored. This exception also has another exception, recently Chromium 116 fixed it and we can now use it, but in the rest of the browsers, both properties will still be ignored.

Duplicated selectors are joined

Lastly, if we write the same <keyframe-selector> more than once, all declaration lists will be valid and the latter won’t replace the previous selector, unless they have different properties, in which case only the property gets replaced. For example, both <keyframe-selector> will get executed in the following @keyframes:

@keyframes remake {
  /* ... */

  50% {
    background-color: blue;
  }

  50% {
    width: 400px;
  }

  /* ... */
}

It would be the same as writing:

@keyframes remake {
  /* ... */

  50% {
    background-color: blue;
    width: 400px;
  }

  /* ... */
}

However, if both declarations have the same property, the latter wins. So in the following @keyframes, only the last declaration gets executed:

@keyframes remake {
  /* ... */

  50% {
    background-color: blue;
  }

  50% {
    background-color: red;
  }

    /* ... */
}

And it would be the same as writing:

@keyframes remake {
  /* ... */

  50% {
    background-color: red;
  }

  /* ... */
}

Demo

Specification

The @keyframes at-rule is defined in the CSS Animations Level 1 specification.

Browser support

The @keyframe at-rule is widely supported by all modern browsers and legacy browsers including Internet Explorer 10+.

More information