A CSS @keyframes rule is not a movie. It is a short list of snapshots, each pinned to a percentage of the animation, and the browser interpolates every frame in between. Internalise "snapshot at a percentage" and CSS animation stops feeling magical. I built a little tool that lets you drag keyframe stops on a timeline, tune each one, watch it play, and copy the exact rule — all in vanilla JS with a dynamically injected <style> tag. It has one genuinely clever move and a lot of easy plumbing; here's the clever move.
The whole state is a name plus a list of stops
An animation is a named rule plus playback settings, and inside it a list of stops. Each stop is one percentage and the property values the element should have there. Keep it all in one plain object — that object is your document. Note that the playback fields live here too but they belong to the animation shorthand, not inside @keyframes.
letanim={name:"myAnim",duration:1500,// ms — one looptiming:"ease",iteration:"infinite",direction:"normal",stops:[{pct:0,tx:0,ty:0,scale:1,rotate:0,opacity:1,bg:"#f59e0b"},{pct:50,tx:0,ty:0,scale:1.4,rotate:180,opacity:.6,bg:"#3b82f6"},{pct:100,tx:0,ty:0,scale:1,rotate:0,opacity:1,bg:"#f59e0b"}]};
Serialise stops into a rule
Turn each stop into the CSS that goes inside its percentage braces, then wrap them under @keyframes name. One detail matters: all the movement, size and tilt live in a singletransform — translate, then scale, then rotate — because a second transform line would overwrite the first, and the order inside it changes the result (rotate-then-translate moves along the rotated axis, which is usually not what you want).
Write CSS at runtime — the injected
</h2>
<p>This is what makes the preview <em>real</em>. Create one <code><style></code> element, keep a handle to it, and overwrite its <code>textContent</code> with the freshly generated rule whenever the state changes. The browser's actual CSS engine now owns the interpolation — you never compute a single in-between frame yourself. Three canvas shapes can share the one named animation, and editing a stop re-injects the rule for all of them.<br>
</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">styleEl</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">head</span><span class="p">.</span><span class="nf">appendChild</span><span class="p">(</span><span class="nx">styleEl</span><span class="p">);</span> <span class="c1">// one tag, reused forever</span>
<span class="kd">function</span> <span class="nf">writeRule</span><span class="p">(){</span> <span class="nx">styleEl</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nf">toKeyframes</span><span class="p">(</span><span class="nx">anim</span><span class="p">);</span> <span class="p">}</span>
</code></pre></div>
<p></p>
<h2>
<a name="the-one-real-gotcha-none-→-reflow-→-readd" href="#the-one-real-gotcha-none-→-reflow-→-readd" class="anchor">
</a>
The one real gotcha: none → reflow → re-add
</h2>
<p>Here's the trap that trips everyone. A browser will <strong>not</strong> pick up changes to a <code>@keyframes</code> rule for an animation that is already running, and re-assigning the same <code>animation</code> string does nothing either. So after every edit you have to force a restart: set <code>animation</code> to <code>none</code>, then <em>read</em> a layout property (<code>offsetWidth</code>) to force a synchronous reflow so the browser commits the "no animation" state, then set the animation back. Without that middle line the two assignments collapse into one and nothing restarts.<br>
</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">function</span> <span class="nf">apply</span><span class="p">(</span><span class="nx">el</span><span class="p">){</span>
<span class="nf">writeRule</span><span class="p">();</span>
<span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">animation</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">;</span>
<span class="k">void</span> <span class="nx">el</span><span class="p">.</span><span class="nx">offsetWidth</span><span class="p">;</span> <span class="c1">// <-- force reflow (the crux)</span>
<span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">animation</span> <span class="o">=</span> <span class="nx">anim</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">"</span><span class="s2"> </span><span class="dl">"</span> <span class="o">+</span> <span class="p">(</span><span class="nx">anim</span><span class="p">.</span><span class="nx">duration</span><span class="o">/</span><span class="mi">1000</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">s </span><span class="dl">"</span>
<span class="o">+</span> <span class="nx">anim</span><span class="p">.</span><span class="nx">timing</span> <span class="o">+</span> <span class="dl">"</span><span class="s2"> </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">anim</span><span class="p">.</span><span class="nx">iteration</span> <span class="o">+</span> <span class="dl">"</span><span class="s2"> </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">anim</span><span class="p">.</span><span class="nx">direction</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p></p>
<p>That single <code>void el.offsetWidth</code> is the whole reason the tool can show your edits live. It's one of those lines that looks like dead code and is actually load-bearing.</p>
<h2>
<a name="everything-else-is-data" href="#everything-else-is-data" class="anchor">
</a>
Everything else is data
</h2>
<p>Once the replay works, the rest is plumbing. The draggable timeline is one <code><div></code> with a marker per stop positioned by <code>left: pct%</code>; on pointer-move I convert the cursor's x into 0–100 and write it back onto that stop. "Add stop" finds the widest gap between consecutive percentages and drops a new stop in the middle, cloning its left neighbour so nothing visually jumps. Play/pause flips <code>animation-play-state</code> between <code>running</code> and <code>paused</code> so the element freezes exactly where it is. And every preset — pulse, bounce, shake, spin — is just a canned list of stops you drop straight into state.</p>
<p>Two things worth burning in that the tool surfaces along the way: percentages are portions of <em>one loop</em>, not seconds (change the duration and the stops stay put while the real time stretches), and animating <code>transform</code> / <code>opacity</code> is the fast path because the compositor handles it with no layout — prefer <code>translate</code> over <code>top/left</code>, <code>scale</code> over <code>width/height</code>.</p>
<p>The lesson underneath: a keyframe animation is not frames you draw. It's a handful of snapshots at percentages, the browser tweens everything between, and your job is just to write the CSS and restart it.</p>
<p>Drag the stops and copy the rule:<br>
<a href="https://dev48v.infy.uk/solve/day37-keyframes-builder.html">https://dev48v.infy.uk/solve/day37-keyframes-builder.html</a></p>
Top comments (0)
Subscribe
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)