I keep finding animations I want to take apart. A toast that lands with a little bounce, a progress bar with a nice ease. Chrome DevTools lets you pause and scrub them in the Animations panel. It won't give you the code, so you end up reading keyframes off a timeline and retyping them.
So I built Motion Inspector, a Chrome extension that pauses and scrubs whatever is animating on a page and exports each animation as CSS, a CSS linear() easing, Motion or GSAP. It took one week and 44 commits, and it's on GitHub at v0.3.0. Here is how it works, and where it still gets things wrong.
Why a side panel
A popup closes the moment you click the page, and plenty of the animations worth inspecting only run on hover. A DevTools panel means opening DevTools, which steals width and shifts the very layout you're looking at. Chrome's side panel stays open while you hover, click and scroll, and it follows the active tab. That settled it before I wrote any code.

The permissions follow from that choice. It asks for sidePanel, scripting and access to all sites, but it declares no content scripts, so nothing runs on a page until the panel is open on it. I considered activeTab and dropped it. Its grant is lost on every navigation, and a panel that sits open across reloads would keep losing access.
Catching animations that are already gone
document.getAnimations() lists CSS animations, transitions and Web Animations. The catch is that a hover transition finishes in about 200 ms and drops out of that list, so a panel polling every 250 ms misses most of them.
The panel injects a small agent into the page's MAIN world, the only world that can see window.gsap and patch the page's own prototypes. It wraps Element.prototype.animate, so every WAAPI call, Motion's included, is registered the moment it's created. It listens in the capture phase for animationstart, transitionrun and transitionstart. And it keeps finished animations in a registry capped at 100, so a transition can still be exported after it ends.
My workflow is to pause first, then hover. Anything that starts while the page is paused gets held on arrival.
Every command from the panel is one chrome.scripting.executeScript call into that agent. There is no message bridge and no long-lived port to reconnect.
Springs come back as springs
Motion hands a spring to the browser as a linear() curve with dozens of sampled stops. Exporting that verbatim is correct and useless to a person.
So the exporter first checks whether a linear() curve looks like a spring: it overshoots, or it settles well before the end. If it does, the exporter grid-searches damping ratio and natural frequency against the curve, refines the best match locally, and accepts it when the RMS error is under 1.5%. On the spring in my test page, the Motion tab gives back this:
animate(
"div.box.green.spring",
{
transform: ["translateY(-50px) scale(0.9)", "translateY(0px) scale(1)"],
},
{
type: "spring",
visualDuration: 0.5,
bounce: 0.4,
delay: 0.3,
repeat: Infinity,
}
)That is what you would have typed in the first place. GSAP has its own mapping. Known bezier curves map back to names like power4.out, and a tween that came from GSAP is echoed with its original vars and ease name.

A waveform instead of a progress bar
The first version had a plain slider, and while the page played it looked like something loading. For 0.3.0 I wrote up how music and video players lay out their transport, built three options, and kept the SoundCloud-style one. Each bar's height is how many animations are running at that moment, so you can see where the busy part of the page is before you scrub to it. Hovering previews a time and what runs there without moving anything.

The bars are laid out in whole device pixels from a ResizeObserver, and each one is clipped to its height with clip-path instead of being resized, so a change repaints without a reflow. The end-to-end run checks for horizontal overflow at panel widths from 300 to 600px, because an earlier build let the playhead push the panel sideways and cut off its left half.
What it can't do yet
Anything driven by requestAnimationFrame has no timing model to read. For Motion values on the main thread, React Spring or a hand-rolled loop, the panel flags the element as JS-driven when its inline style changes eight or more times a second, and that is all it can do. Canvas, WebGL and cross-origin iframes are out of reach.
The exports have a known gap too. A GSAP tween with repeatDelay keeps its pause in the GSAP export, but the CSS export drops it without a note, so the loop runs back to back. CSS has no delay between iterations, so the fix is either a comment in the output saying so or folding the gap into the keyframe percentages. The Motion export loses it as well. That's the next thing I'm fixing.
Where it leaves me
I can pause any page, scrub to the frame I care about, and copy the animation out in the format I'm working in. If your team has a browser chore like this that someone does by hand every week, that's the kind of tool I build.





