CSS Animations Don't Rewind: How to Replay and Slow Them Down
CSS-only 3D demos like the “Wolfenstein” experiment are, under the hood, little more than a long, carefully scripted animation driven by checkbox hacks. The environments are plain 3D grid faces with translations and rotations. But even in deceptively simple setups, two interactions are notoriously hard to pull off cleanly: replaying a “fire” animation on every click, and entering slow motion at just the right moment. Digging into why reveals a useful truth about how browsers handle running CSS animations.
Replaying an Animation
The naive approach—adding a second checkbox that triggers the same animation—fails. Each checkbox works individually, but once one is checked, the other has no effect because the element’s animation-name is still spin. No animation is added or removed, so the running animation simply continues.
Solution: Add a New Animation
The reliable pattern is to avoid restarting an existing animation and instead inject a fresh one. Two variations work:
Cloning: Define two identical keyframe rules (spin1, spin2). Clicking the first checkbox sets animation-name: spin1; clicking the second changes it to spin2. Each new name triggers a fresh start. Note the order matters: the rule for the second checkbox must come later in the stylesheet, so checking it first won’t work.
Appending: Use a comma-separated list, e.g., animation: spin1 2s, spin2 2s. When the second checkbox is checked, the still-running first animation is demoted to the second entry in the list. This approach breaks if animation-fill-mode: forwards is set, because the finished spin1 then overrides the newly started spin2. With the default fill-mode: none, only the actively playing animation has any visual effect.
Once an animation has started it continues until it ends or the
animation-nameis removed.
For most practical cases, cloning is the better default, especially when a CSS preprocessor is in play.
Slowing an Animation Down
Simply increasing animation-duration mid-run does slow the animation—but it also causes a visible jump. The spec explains why:
Changes to the values of animation properties while the animation is running apply as if the animation had those values from when it began.
In other words, changing the duration retroactively re-scales the entire timeline. The element instantly switches to the state it would have been in had the new duration applied from the start. If the animation has progressed 50% under a 4s duration, switching to 8s makes it appear only 25% complete—hence the jump to an arbitrary-looking position.
Passing State Between Animations
A better approach pauses the current animation and starts a slower one from the same spot. Without a from keyframe, the new animation begins at its default starting point, which is wrong. But you can capture the current angle with a custom property and feed it into the second animation’s from keyframe. This works but relies on @property, which has limited browser support. It also handles “exiting” slow motion poorly—restoring normal speed breaks the seamlessness.
Gear Shifting
A CSS-only workaround models the problem like shifting gears. Two nested <div>s each run the same animation with different durations; the element’s final state is the accumulation of both. One is running and the other paused at the start. Toggling the checkbox swaps which one runs. This achieves seamless entry and exit without experimental features, but it exploits the specific symmetry of a simple, looping rotation—it doesn’t generalize to arbitrary keyframes.
The General Fix: JavaScript and Negative Delay
For arbitrary animations, the robust method uses a small amount of JavaScript:
- Track animation progress via a custom property.
- On toggle, “restart” by swapping to a new animation.
- Compute a negative
animation-delayvalue so the new animation starts exactly where the old one left off (negative delays start animations partway through, as described in this guide).
If @property isn’t available, even z-index can be used to store progress. This approach is effectively a hybrid of restarting and gear-shifting. It can be extended to nearly any keyframe setup—even those with multiple animations or non-linear timing functions—by carefully converting the current progress into the correct delay. The same math underpins the Web Animations API’s method for updating playback rate seamlessly.
The fundamental constraint is that CSS animations are stateless per the spec—browsers don’t track “current position,” they recompute it from properties whenever anything changes. Because animation-delay itself is not animatable, a purely CSS solution to slow motion with arbitrary keyframes remains elusive.



