Delaying CSS Keyframe Animations Between Iterations

CSS offers animation-delay to postpone the initial run of an @keyframes animation, but there is no native property for inserting a pause between subsequent iterations. If you need a beat of stillness after each loop — say, to make a shooting-stars background feel more random — you have to approximate it with extra keyframes.

The Compression Approach

The classic workaround is to shrink the animation timeline so that the actual motion occupies only part of the 0%–100% range, and the remaining time holds the final frame. That means choosing a delay duration, dividing it by the total iteration time, and restructuring every keyframe percentage accordingly.

@keyframes my-animation {
  /* Animation happens between 0% and 50% */
  0% {
    width: 0;
  }
  15% {
    width: 100px;
  }
  /* Animation is paused/delayed between 50% and 100% */
  50%, 100% {
    width: 0;
  }
}

The method works, but it is brittle. Every keyframe must be hand-calculated, and debugging requires mentally expanding the keyframes back to full scale. Reading such an animation on its own makes it unclear how the motion actually appears in real time.

Hiding the Element During the Pause

A more modular technique uses a second set of @keyframes whose sole job is to conceal the element during the delay window. Apply that alongside the original animation, and the element becomes invisible right after finishing a cycle, only to reappear when the next iteration begins.

.target-of-animation {
  animation: my-awesome-beboop 1s, pause-between-iterations 4s;
}

@keyframes my-awesome-beboop {
  ...
}

@keyframes pause-between-iterations {
  /* Other animation is visible for 25% of the time */
  0% {
    opacity: 1;
  }
  25% {
    opacity: 1;
  }
  /* Other animation is hidden for 75% of the time */
  25.1% {
    opacity: 0;	
  }
  100% {
    opacity: 0;
  }
}

The critical constraint is that the pause must be an integer multiple of the animation duration. Both keyframe sets run on an infinite loop concurrently; if the delay animation lasts three times as long as the main one, the main animation executes three times in the background before the element is revealed again. The user sees what looks like the second loop even though the animation has already run five times.

Counteracting Motion When You Can’t Hide the Element

If the element must remain visible during the pause, you can keep the delay keyframes but turn them into motion cancellers. Instead of hiding, animate another CSS property to negate the primary animation’s movement. A ball traveling with translateX, for instance, can be held in place by simultaneously animating left in the opposite direction.

/* pausing the animation for three iterations */
@keyframes slide-left-pause {
  25%, 50%, 75% {
    left: 0;
  }
  37.5%, 62.5%, 87.5% {
    left: -100px;
  }
  100% {
    left: 0;
  }
}

For more than a single paused iteration, those compensating keyframes get more complex, and you may observe slight jitter. In the translateX case, the two competing animations cause minor vibration while the pause keyframes fight for control of the element’s position.

Performance Considerations

Prefer hiding the element with opacity or animating transform when performance matters. Layout-triggering properties such as left and margin place heavier load on the processor, though the contain property can help mitigate that cost.

A final note on timing functions: easing is not applied across the entire 0%–100% timeline. Each easing curve applies between the keyframes that define the start and end value of a given property. With keyframes at 25% { left: 0 } and 75% { left: 50px }, the easing runs only from 25% to 75%, not from the beginning. This granular behavior is easy to overlook until you build an animation that depends on it.