Imoran ai LogoImoran ai Logo

Building Motion Inspector: Turning Any Page's Animations Into Code

Why I built a Chrome side panel extension that pauses, scrubs and exports a page's animations as CSS, linear(), Motion or GSAP, how it catches 200 ms transitions, and where its exports still fall short.

Published: November 2, 2026

Last updated: November 2, 2026

The Motion Inspector side panel in dark mode, paused on a spring animation called drop, its overshooting easing curve above a waveform scrubber.

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.

Motion Inspector docked in Chrome's side panel beside a demo deploys page, with the hovered load row's progress bar outlined on the page.

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.

The toast-in spring curve beside its Motion export, where the sampled linear() easing comes back as type spring with a bounce of 0.18.

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 global waveform previewing 1260 ms with two animations running, next to a single animation's scrubber drawn from its eased value.

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.

Frequently asked questions

How is this different from the Animations panel in Chrome DevTools?

DevTools can inspect, pause and scrub animations, but it doesn't give you code. Motion Inspector exports each animation as CSS, a CSS linear() easing, Motion or GSAP, and it lives in Chrome's side panel, so it stays open while you hover and scroll the page.

Does it work with Motion (formerly Framer Motion)?

Partly. Values Motion hands to the Web Animations API (opacity, transform, filter, clipPath and springs on those) are captured, paused and exported. Values Motion runs on the main thread show up as JS-driven, with no controls and no export.

How do I install it?

It isn't on the Chrome Web Store. Download the zip from the GitHub Releases page, unzip it, turn on Developer mode at chrome://extensions and load the folder unpacked. It needs Chrome 116 or newer for the side panel API.

Can you build something like this for my team?

Yes. If your team keeps doing the same fiddly thing in the browser by hand, a focused extension or dev tool usually fixes it. Tell me the workflow and I'll map out an approach.

More Articles

Let's Work
Together

Hand Reaching

Have a project in mind? We'd love to hear about it. Let's create something great together!

PRODUCT DEVELOPERUI ENGINEERMVP DEVELOPMENTFRONTEND DEVELOPMENTPRODUCT DEVELOPERUI ENGINEERMVP DEVELOPMENTFRONTEND DEVELOPMENTPRODUCT DEVELOPERUI ENGINEERMVP DEVELOPMENTFRONTEND DEVELOPMENT