motionlint
About
atch bad animations before they ship. Deterministic motion audit + vision-LLM design review.
Details
- Author
- bobaba99
- Categories
- Design, AI
Jump to
Setup
Install motionlint in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/bobaba99/motionlint
Follow the installation instructions in the repository README, then restart your MCP client.
Score any page's animation quality in one command. No API key, no config.
npx motionlint audit http://localhost:3000 --open
Deterministic — measured from the live page, no LLM involved. One-time prerequisite:npx playwright install chromium.
MotionLint measures the motion your app actually ships — durations, easing curves, stagger intervals, exit timing, reduced-motion support — and scores it against a published set ofanimation standards. Ease-in on a dropdown, a 600ms modal, a card that scales from 0, hover motion that fires on touch: all caught, all with the measured value and a concrete fix.
The audit is free and offline. Add an API key and MotionLint also doesvision-LLM design review— multi-viewport screenshots and 50ms frame bursts of real user journeys, judged by a model and handed back to your coding agent as ranked findings. It runs as an MCP server inside Claude Code and Cursor.
AI coding agents read JSX, HTML, and CSS — they're blind to what the user actually sees, clicks, and watches animate. Rules in a prompt tell the agent whatshouldhappen; nothing checks whatdid. Modals that should slide in just pop; loading states get omitted; focus rings disappear. Code review can't catch any of this before merge, because none of it is visible in the diff.
MotionLint closes that loop: it measures the running app and feeds the verdict back.
The conceptual gap MotionLint closes: visual-regression tools catch whatchangedbut not whether the new pixels aregood; AI design tools generate from scratch but don't review what's already running. MotionLint reviews live behavior with a vision LLM and feeds the verdict back into the coding loop.
npx playwright install chromium # one-time per machine (~300MB) npx motionlint audit http://localhost:3000 --open
That's the whole setup for the audit. It's deterministic, runs offline, costs nothing, and works on any URL you can load — your dev server, a staging deploy, or someone else's site. Requires Node 18+.
The rules it checks are published indocs/STANDARDS.md— read them before you install anything.
Set one API key (ANTHROPIC_API_KEY,OPENAI_API_KEY, orGOOGLE_API_KEY— or run Ollama locally for free) and three more commands unlock:
npm install -g motionlint # Multi-viewport UX review of a page → ranked findings across 12 dimensions. motionlint review http://localhost:3000 # Animation review of a scripted user journey → frame contact sheet + report. motionlint flow --spec flows/signup.json # Interactive HTML tuner — every animation on the page, with live sliders. motionlint tune http://localhost:3000
claude mcp add motionlint -- npx -y motionlint mcp
# CI mode — non-zero exit on critical issues, SARIF output for code scanning. motionlint review https://staging.acme.dev --ci --threshold critical --format sarif -o ux.sarif # Polished, shareable HTML review with embedded screenshots + before/after fixes. motionlint review http://localhost:3000 --format html -o review.html # Review every route the site knows about (sitemap.xml + Next.js app/ directory). motionlint review http://localhost:3000 --discover-routes # Storybook mode — discover stories from /index.json, review each story iframe as its own route. motionlint review http://localhost:6006 --storybook # Color-scheme sweep — light and dark modes, plus Windows High Contrast. motionlint review http://localhost:3000 --schemes --forced-colors --format html -o review.html # Interaction affordances — grid each element's default/hover/focus/active states. motionlint review http://localhost:3000 --state-grid # Agent focus — keep only the top 5 findings, and only ones not seen in prior runs. motionlint review http://localhost:3000 --max-findings 5 --new-only # Before/after comparison — PR preview vs. production baseline. motionlint review https://pr-123.preview.example.com --against https://prod.example.com # Reviewer focus — cap the SARIF upload at 10 annotations per report. motionlint review https://staging.acme.dev --format sarif -o ux.sarif --max-pr-annotations 10 # Pick a provider explicitly (auto-detect picks the first reachable one). motionlint review http://localhost:3000 --provider anthropic --model claude-sonnet-5 # Track provider quality across runs + teach the reviewer from eval misses. motionlint eval --provider anthropic --evolve
Sample terminal output for a flow review:
$ motionlint flow --spec flows/signup.json --provider anthropic → Running flow "signup-happy-path" against http://localhost:3000/signup (11 steps, 50ms intervals × 750ms window) provider: anthropic (claude-sonnet-5) capturing flow… ✓ step 1: 16 frames ✓ step 2: 16 frames ✓ step 3: 16 frames … captured 176 frames in 31s contact sheet → .motionlint/flows/signup-happy-path-…png analyzing flow… report → .motionlint/flows/signup-happy-path.md Score: 4/10 · 3 critical findings [critical] interaction — input focus rings missing across steps 2/4/6 [critical] interaction — submit button has no pressed state [critical] loading_state — 1.4s wait with no spinner during submit
A multi-route TS animation showcase ships indemo/— covering Motion One, GSAP, anime.js, @formkit/auto-animate, and lottie-web — including a cat-themed one-pager that exercises every MotionLint capability in a single URL:
node demo/server.mjs # http://localhost:4173 motionlint review http://localhost:4173/cat --record --embed motionlint flow --spec flows/signup.json motionlint tune http://localhost:4173/dashboard
Routes available:/,/pricing,/signup,/dashboard,/loading,/cat. Reports go to.motionlint/reports/, screenshots to.motionlint/screenshots/, videos to.motionlint/videos/.
MotionLint auto-loads a.envfile from the working directory at startup:
# .env (gitignored) ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=sk-... # or GOOGLE_API_KEY=... # or run a local Ollama (no key needed) — auto-detected on http://localhost:11434
Real environment variables take precedence over.env. With no key set and no Ollama running, MotionLint falls back to a deterministicmock providerso the full pipeline (capture → analysis → report) still runs end-to-end for smoke tests.
MotionLint auto-detects in this order:Ollama (local) → Anthropic → OpenAI → Google. The first one with a working API key (or running service) wins. Override with--provider <name>and--model <id>. SeeProviders in depthfor the per-provider quality scorecard and how to pick.
Everything below is for readers who want to understand how MotionLint works under the hood, pick the right provider for their workflow, or wire it into CI.
The flow-review pipeline was stress-tested across12 popular web-app animation patterns × 2 variants(24 fixtures total) — staggered entrances, hover/press/focus, modal entrances, loading skeletons, form errors, toasts, counter ramps, multi-animation dashboards, modal-with-content stagger, rich form feedback (focus + press + spinner + success), and scroll-driven animations (progress bar + IntersectionObserver reveal + parallax).
Run on2026-07-27against the current flagship from each major provider:
Read this as: recall is no longer a differentiator.Every current flagship catches all 12 seeded faults. That is the finding — a year ago it wasn't true, and it means the model choice no longer decides whether MotionLint works. Pick on cost and latency.
Do not rank these models on the FPR column.A single 24-fixture run cannot resolve it. Across two clean runs of the identical suite, with nothing changed but sampling, FPR moved by 1–2 fixtures per model —gpt-5.51/12 → 0/12,gpt-5.6-sol2/12 → 0/12,gemini-3.6-flash3/12 → 2/12. One fixture is 8 percentage points, so the entire spread between "0%" and "17%" sits inside the noise floor. Treat the column as"all of these occasionally flag something clean", not as a ranking.
The first 2026-07-27 run of this suite putclaude-opus-5at 83% recall — last among all five models — and the 2026-04-29 edition of this table reported several models at 0% FPR. Both were artifacts of a MotionLint bug, not model behaviour.
Anthropic'smax_tokensdefaulted to 4096. Verbose responses hit the ceiling mid-JSON, and the unparseable result was scored as0/10, no issues found— indistinguishable from a clean review. Two of Opus 5's three truncations landed on broken fixtures, which produced the entire 83% figure.
The same bug deflated FPR everywhere: a truncated review reportsnothing, so it cannot raise a false positive. Any historical "0% FPR" was partly measuring broken parsing rather than model precision. Fixed 2026-07-27, along with the sibling paths that turned truncated, refused, and safety-blocked responses into clean-looking results.
Full per-provider scorecards in.motionlint/stress/after runningscripts/run-all-benchmarks.mjs. Use--only <provider>:<model>to re-run a single model.
¹Measured, not estimated— one realmotionlint reviewper model against the demo app at the default 2 viewports, full-page, reading actual token counts from each provider's usage field and multiplying by published list price. Reproduce withformatUsageLine()on any run. Sonnet 5 uses its introductory rate (through 2026-08-31); it roughly rises by half after that. Flow review sends one composite image per flow but the contact sheet is larger. The Animation Tuner andmotionlint auditmakezeroLLM calls and cost nothing.
Output tokens dominate.Input is within 2× across all five models; output spans 1,390 (Gemini) to 10,219 (Opus 5). That 7× spread, not image size, is what makes the most expensive model 15× the cheapest.
- Default.Googlegemini-3.6-flash— 100% recall,13× cheaperthan Opus 5 and the fastest of the five (4.9 min). Since every model caught every fault, there is no quality argument for paying more by default.
- Anthropic house.claude-sonnet-5at $0.089 — 3× cheaper thanclaude-opus-5with identical recall. Opus 5 costs more and took2× the wall time(21.0 min vs 10.8) for no measured recall advantage; reach for it only if you value its slightly higher score gap (+4.1 vs +3.1).
- OpenAI house.gpt-5.5andgpt-5.6-solare indistinguishable on every measured axis and within 7% on price. Take whichever your account already has.
- Hard CI gate.Any of them on recall. Do not pick on FPR — see the noise-floor caveat above. If false positives matter to your gate, run your own fixtures rather than trusting a single 24-fixture run of ours.
- Local / air-gapped.Any Ollama vision model works, but confirm itisvision-capable: some accept images over the API, silently ignore them, and answer from the prompt alone. None was benchmarked in this run.
Every command honours--providerand--model:
motionlint review http://localhost:3000 --provider openai --model gpt-5.5 motionlint flow --spec flows/signup.json --provider google --model gemini-3.6-flash motionlint review http://localhost:3000 --provider ollama --model llava:13b
To compare a new provider against the same 24-fixture stress test:
node -e " import('./dist/config/env.js').then(async ({ loadEnv }) => { loadEnv(); const { runStress, renderStressMarkdown } = await import('./dist/flow/stress.js'); const { writeFile, mkdir } = await import('node:fs/promises'); const { resolve } = await import('node:path'); await mkdir('.motionlint/stress', { recursive: true }); const r = await runStress({ stressPath: resolve('eval/animation-stress.json'), fixturesDir: resolve('eval/animation-fixtures'), artifactDir: resolve('.motionlint/stress'), provider: 'YOUR_PROVIDER', // 'openai' | 'google' | 'ollama' }); await writeFile('.motionlint/stress/SCORECARD.md', renderStressMarkdown(r), 'utf8'); console.error('Recall:', (r.broken_recall100).toFixed(0)+'%, FPR:', (r.good_false_positive_rate100).toFixed(0)+'%, gap:', r.avg_score_gap.toFixed(1)); }); "
Open.motionlint/stress/SCORECARD.mdfor the per-pattern breakdown.
Static screenshots can't tell you whether a flow's animations and interaction states work — only whether the final frame looks right.motionlint flowfills that gap.
- Runs the journey in headless Chromium via Playwright — clicking, typing, hovering, scrolling, pressing keys exactly like a user would.
- Captures aburst of 16 frames over 750ms (50ms intervals) after every interactionvia CDP screencast (Page.captureScreenshotJPEG, ~8ms per shot). 50ms is half the human visual-detection threshold and below the industry-typical 100ms minimum animation interval — short animations like 100ms button presses get caught with 2-3 mid-state frames. Every interaction burst is also pixel-diffed for input→feedback latency — interactions with no visible acknowledgment within the burst window are flagged deterministically.
- Records thefull Playwright videoas an artifact you can scrub later.
- Composites every burst into a labeledcontact sheet— one row per step, frames laid out in sub-rows.
- Sends the sheet to the vision LLM with a flow-aware rubric covering: missing animations, buggy/janky animations, missing loading states, perceived performance, affordance & state changes, choreography, smoothness, accidental flicker, navigation continuity, reduced-motion respect.
- Produces a Markdown report with per-step trace, ranked findings, and a"Prompt for Claude Code"block at the bottom — paste it into CC and it acts on the findings directly.
A single recording can capture and analyze multiple concurrent animations. Validated on:
- Dashboard reveal(3 concurrent: tile stagger + counter ramps + chart bar rise)
- Modal stack(backdrop fade + modal slide+fade + inner content stagger)
- Rich form feedback(focus ring + button press + loading spinner + success card)
- Scroll-driven(scroll-progress bar + IntersectionObserver section reveal + parallax hero)
The LLM correctly identifieswhichanimations are broken without false-flagging the working ones — see the validated-quality table.
For sites with scroll-linked animations,scroll <px>steps animate the scroll over the burst window viarequestAnimationFrameso each frame shows progressive scroll position and the LLM sees the timing as the page scrolls.
# Inline DSL — semicolon-separated steps motionlint flow \ --url http://localhost:3000 \ --steps "navigate /signup; click input#email; type input#email=ada@example.com; click button[type=submit]; wait 2000; capture \"post-submit\"" \ --name signup-happy-path # Or load a structured spec with expected_animations[] hints motionlint flow --spec flows/signup.json --provider anthropic # Pass team motion preferences (philosophy + inspirations + accepted defaults) # Embedded into the prompt AND the report's CC handoff block. motionlint flow --spec flows/signup.json --preferences flows/preferences.md # Tighten the interval below 50ms for fine-grained timing review motionlint flow --spec flows/signup.json --interval 30 --burst-ms 600 # Auto-detect: scan the page's animations, pick an interval that captures # the shortest one with 4 frames inside it (clamped to [20, 100]ms). motionlint flow --spec flows/signup.json --auto-interval
Defaults: a frame burst is taken aftereveryinteraction. Pass--no-implicit-burststo only burst on explicitcapturesteps. Pass--no-recordto skip video.
Three ready-to-run sample flows ship in the repo:flows/signup.json,flows/loading-state.json, andflows/preferences.md.
Most AI coding tools generate animations from scratch. The Tuner lets youtune the animations that are already running on your page, in real time, and hand the changes back to your coding agent as a structured prompt.
motionlint tune http://localhost:3000 --open
- Opens your app in headless Chromium with an instrumentation script that hooks the major TS animation libraries (Motion One, GSAP, anime.js, @formkit/auto-animate, lottie-web) plus all CSS transitions and@keyframesrunning on the page.
- Captures every detected animation: the element selector, source library, timing parameters, and bounding box.
- Generates a self-contained interactive HTML page at.motionlint/tuner/index.html(auto-opens with--open):
- Live preview surfaceper animation (Shadow DOM — no iframes, no flash, themed to the source page).
- Slidersfor duration / delay / stagger / speed.
- Easing-preset dropdown— Emil Kowalski's strong curves lead (ease-out, ease-in-out, iOS drawer), then the softer/decorative options.
- Inline standards linting— each card flags where the animation deviates from the motion standards (severity badge, fix, suggested value), with a header score.
- Comments boxper animation for design rationale.
$ motionlint tune http://localhost:3000 → Capturing animations on http://localhost:3000… detected 15 animation(s) tuner → /Users/you/proj/.motionlint/tuner/index.html open with: file:///Users/you/proj/.motionlint/tuner/index.html
motionlint audit http://localhost:3000 --open # polished HTML report, scored 0–100 motionlint audit http://localhost:3000 --json audit.json --ci # machine-readable; non-zero on critical
Add--layoutto also lint layout (tap targets, text size, contrast, overflow) from live DOM measurements — still deterministic, still no API key.
Add--watch [dir]to re-run the audit on file changes under[dir](default: cwd) and print the score with a delta after each run — a live readout while you iterate. Recursive watching requires macOS, Windows, or Linux with Node 20+.
The report pairs every finding with abefore → afterpanel; easing findings render a live cubic-bezier curve comparison so the fix is visible, not just described. The same standards feed theflowreview prompt (so vision findings cite concrete rules) and appear inline in the Animation Tuner.
MCP server — tools, resources, deployment
MotionLint ships an MCP server over stdio so an LLM agent can drive it directly inside a chat. Themotionlint mcpsubcommand boots it; the agent client spawns the process when a tool is called.
claude mcp add motionlint -- npx -y motionlint mcp
Local checkout (handy while developing):
claude mcp add motionlint -- node /absolute/path/to/motionlint/dist/index.js mcp
- Confirm it appears:claude mcp list—motionlintshould show asrunningoravailable.
- Make sure API keys are reachable. The MCP server inherits the env it's spawned in. Cleanest path: drop a.envfile in the project directory you're working from — MotionLint auto-loads it on startup.
- First run:npx playwright install chromiumif you haven't already.
"Use motionlint to review the local app at mobile and desktop and tell me the top 3 issues to fix."
"Run motionlint review_flow onhttp://localhost:3000/signupwith stepsclick input#email; type input#email=test@test.com; click button[type=submit]; wait 2000; captureand check the animations."
"Run motionlint tune_animations onhttp://localhost:3000/pricing— I want to fine-tune the card hover animations."
Resources:motionlint://reports/latest— the most recent report content.
Before deploying or sharing the MCP server with other users:
- Build is fresh.npm run buildthen verifydist/index.jsexists. Without this,motionlint mcpwon't start.
- Playwright Chromium installedon the target machine:npx playwright install chromium. The postinstall hook reminds you, but it's not enforced (we don't auto-download a 300 MB binary onnpm install).
- API keys reachable— either via shell env or via a.envfile in the working directory the MCP client launches from.
- Smoke-test the MCP surface.npm testincludes an MCP smoke test that boots the server, lists tools, and asserts the expected tool surface.
- No secrets committed..envis gitignored;.env.exampleshould be a placeholder. Worth a finalgit diff --cached | grep -i 'sk-\|api_key'before pushing.
- Confirm withclaude mcp listthat the server shows up and isn't erroring at startup.
# .github/workflows/ux.yml - run: npm ci - run: npx playwright install chromium - run: npx motionlint review $STAGING_URL --ci --threshold critical --format sarif -o ux.sarif - uses: github/codeql-action/upload-sarif@v3 with: { sarif_file: ux.sarif }
MotionLint exits with1when critical issues exceed the configured threshold (failOnCritical) — wire it as a status check.
Amotionlint flowcontact sheet — timestamped bursts after each interaction, exactly what the vision model sees.
Analyzes:each screenshot is sent to a vision model with an opinionated UX-review system prompt covering twelve dimensions (hierarchy,spacing,alignment,typography,color,contrast,responsiveness,interaction,content,navigation,consistency,loading_state). For each issue the model returns:
{ "category": "hierarchy", "severity": "critical | warning | suggestion", "location": "above-the-fold hero", "issue": "Primary CTA blends into the background gradient.", "why_it_matters": "Users miss the conversion path on first scroll.", "fix": "Increase background contrast or use a solid surface behind the button." }
Override the prompt with--rules path/to/your-design-rules.mdto inject project-specific heuristics.
Every review capture also takes aDOM snapshot: notable elements (headings, CTAs, inputs) get stable refs (E1,E2, …) with measured pixel rects, listed in the prompt so the model can ground a finding with"element_ref": "E3". Cited refs resolve back to their rects and aredrawn as severity-colored bounding boxes on the screenshotin the HTML report (and reported asWhere: E3 at (x, y) w×hin markdown). Refs the page never listed are dropped — the model can't annotate what it wasn't shown.
With--format htmlthe findings render as a single shareable report — score ring, per-dimension breakdown, and an issue → fix panel per finding with the annotated screenshot:
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.
