AI coding tools have gotten very good at motion. Ask for a landing page and you get parallax heroes, staggered reveals, spring physics on every card. It looks great in the demo.
Here's what it almost never ships with: a prefers-reduced-motion path.
Why this is a real problem, not a checkbox
Vestibular disorders are common. For the people who have them, large parallax movement, zooming, and spinning UI can trigger dizziness, nausea, and migraines. That's why the media query exists, and why WCAG has two success criteria aimed squarely at this:
WCAG 2.2.2 (Pause, Stop, Hide) — Level A. Anything that moves automatically for more than 5 seconds needs a way to pause, stop, or hide it. Level A means baseline, not aspirational.
WCAG 2.3.3 (Animation from Interactions) — motion triggered by interaction should be disableable unless it's essential.
Most AI-generated motion fails both by default. Not because models "don't know" about reduced motion — they'll happily explain it if you ask — but because generation is sampling. Same model, same prompt, different run, different compliance.
Prompting is hope. Verification is a property.
"Make it accessible" in the prompt is not a guarantee, it's a suggestion. The pattern that actually works for LLM-generated code — the argument the Bun team made when they rewrote in Rust with heavy agent involvement — is a conformance suite plus mechanical enforcement. The model can write whatever it wants; the output has to pass the checks.
Motion has no such layer today. axe-core and Lighthouse are excellent, but they largely can't catch a scroll-jacked hero with no reduced-motion path, because statically analyzing dynamic motion behavior is hard. The gap is exactly where AI tools generate the most output.
What verifying motion looks like
The trick is treating motion as data instead of as scattered CSS and JS. If motion is declared in a spec, it becomes checkable:
json{
"element": "hero-title",
"primitive": "fade-rise",
"duration": 600,
"trigger": "scroll",
"reducedMotion": "opacity-only"
}
(Simplified for illustration.)
With motion as a spec, the questions become mechanical:
Does every animated element define reduced-motion behavior?
Does anything auto-play longer than 5s without a pause/stop/hide mechanism?
Are durations and movement distances inside a sane performance budget?
Fail any of these and the check fails — deterministically, every run.
MotionSpec
That's what MotionSpec does. It's open-core: an MIT-licensed npm package (40 motion primitives, 373 automated tests) plus a hosted verification API. It runs as an HTTP API and an MCP server on Cloudflare Workers — which means AI agents can call it inline while generating UI. The same agent that writes the motion has to pass the check before the motion ships.
There's a free motion-check on the site, no signup, if you want to see what your current motion setup fails.
One disambiguation, because the name space is crowded: MotionSpec is not an AI video or animation generator. It's a verification layer for web interface motion — closer to axe-core or Lighthouse than to any of the AI "Motion" tools.
Honest scope
It verifies motion that's described as a spec. It won't magically retrofit arbitrary legacy CSS/JS animation you point it at — the check is only as good as the spec describing the motion. That's the trade: declare motion as data, get verifiability in return.
If you try it, I genuinely want to know where the checks feel too strict or too loose — that calibration is the whole product. → motionspec.dev
Top comments (2)
The line that landed for me is "generation is sampling." That's the part people miss. It isn't that the model doesn't know about prefers-reduced-motion, it's that knowing doesn't survive a re-roll. Same prompt, different run, different compliance. Once you frame it that way, "make it accessible" in the prompt is obviously the wrong layer to put the guarantee at.
I got to the same conclusion from a completely different direction. I build brand asset generators, and the thing that finally worked was refusing to let the model draw the asset at all: the brand lives as data (palette, type, rules), plain local code renders the SVG deterministically, and a separate reviewer pass checks the output against the profile for contrast and clear space, flagging whatever it cannot verify instead of quietly passing it. Different medium, identical shape. Declare it as data, verify mechanically, never trust that the prompt worked.
Your "honest scope" section is the part I respect most. "The check is only as good as the spec describing the motion" is the same boundary we hit, and saying it plainly rather than implying the tool retrofits arbitrary legacy animation is rarer than it should be.
If you ever feel like poking at the analogous case in another medium, I'd genuinely value your read on where our verification lands too strict or too loose, since calibration is clearly the thing you think hardest about: github.com/localplugins/plugins (the visual-guardian subagent is the relevant bit). Zero pressure either way. Good post.
Appreciate this - and "same shape, different medium" is exactly how it reads from my side. Brand-as-data → deterministic local render → a separate reviewer pass is the same move I'm making on motion, and the ethos runs through the whole marketplace:
Green-keeper's "never fakes a pass" is just the test-suite version of the same refusal to let the model grade its own homework.
I read visual-guardian. Two calibration notes, since you asked where it might sit too strict or too loose.
The contrast check leans too strict as a flat minContrast: 4.5. WCAG splits that number: 4.5:1 for normal text, but 3:1 for large text (≥24px, or ≥18.66px bold) under 1.4.3, and 3:1 for graphical objects and UI components under 1.4.11. A logo mark, an icon, or a display headline that's fine at 3:1 gets flagged as a "fix" it doesn't actually need. Make the threshold role-aware - body vs. large vs. non-text - and the only fixes it raises are real ones.
The second is your honest-scope twin. The guardian reads declared hex, but rendered color isn't always a declared value — overlapping alpha fills, gradients, or a mark sitting on a photographic panel composite into something the source never names, so contrast-as-seen can diverge from contrast-as-declared. You already flag what you can't verify instead of guessing; I'd just make composited color an explicit trigger for that path, since it's the exact case where a computed number would look authoritative and be wrong. Same boundary on both sides: the check is only as good as the spec.
Good stuff. If you ever want to compare notes on the reviewer-pass pattern across mediums, I'm up for it. 🚀