{
  "markdown": "# MotionLint\n\n[![npm version](https://img.shields.io/npm/v/motionlint)](https://www.npmjs.com/package/motionlint) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/bobaba99/motionlint/actions/workflows/ci.yml/badge.svg)](https://github.com/bobaba99/motionlint/actions/workflows/ci.yml)\n\n**Score any page's animation quality in one command. No API key, no config.**\n\n```bash\nnpx motionlint audit http://localhost:3000 --open\n```\n\n<p align=\"center\">\n  <img src=\"docs/media/cli-audit.gif\" width=\"800\" alt=\"motionlint audit running in a terminal: the demo app's /loading route scores 64/100 with findings across duration, easing and accessibility\">\n</p>\n<p align=\"center\"><sub>Deterministic — measured from the live page, no LLM involved. One-time prerequisite: <code>npx playwright install chromium</code>.</sub></p>\n\nMotionLint measures the motion your app actually ships — durations, easing curves, stagger intervals, exit timing, reduced-motion support — and scores it against a published set of [animation standards](docs/STANDARDS.md). 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.\n\nThe audit is free and offline. Add an API key and MotionLint also does **vision-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.\n\n## Why this exists\n\nAI 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 what *should* happen; nothing checks what *did*. 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.\n\nMotionLint closes that loop: it measures the running app and feeds the verdict back.\n\n## How it's different\n\n| | MotionLint | Visual regression tools (Percy, Chromatic, Playwright snapshots) | AI design generators (v0, Galileo, Claude Design, Stitch) |\n| --- | --- | --- | --- |\n| **Deterministic motion audit** | **13 checks, measured from the live page — no API key, $0** | ✗ | ✗ |\n| Multi-viewport UX review | ranked findings across 12 dimensions | pixel diffs only | generates new layouts from prompts |\n| **Animation review** | **50ms frame bursts via CDP screencast → contact sheet → LLM** | ✗ | ✗ |\n| **Live animation tuning** | **Shadow-DOM previews + sliders + Claude Code export** | ✗ | generates new motion, doesn't tune what's there |\n| Native MCP server | ✓ stdio MCP for Claude Code / Cursor | ✗ | varies |\n| CI gate | ✓ SARIF + exit codes for code scanning | ✓ image diff thresholds | ✗ |\n| Validated quality | **100% recall on a 24-fixture stress test, across 5 frontier models** | n/a | n/a |\n\nThe conceptual gap MotionLint closes: visual-regression tools catch what *changed* but not whether the new pixels are *good*; 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.\n\n## Start here — no API key needed\n\n```bash\nnpx playwright install chromium          # one-time per machine (~300MB)\nnpx motionlint audit http://localhost:3000 --open\n```\n\nThat'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+.\n\nThe rules it checks are published in [docs/STANDARDS.md](docs/STANDARDS.md) — read them before you install anything.\n\n## Then: LLM design review\n\nSet one API key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_API_KEY` — or run Ollama locally for free) and three more commands unlock:\n\n```bash\nnpm install -g motionlint\n\n# Multi-viewport UX review of a page → ranked findings across 12 dimensions.\nmotionlint review http://localhost:3000\n\n# Animation review of a scripted user journey → frame contact sheet + report.\nmotionlint flow --spec flows/signup.json\n\n# Interactive HTML tuner — every animation on the page, with live sliders.\nmotionlint tune http://localhost:3000\n```\n\n### Inside Claude Code / Cursor\n\n```bash\nclaude mcp add motionlint -- npx -y motionlint mcp\n```\n\n<details>\n<summary><b>Full flag surface</b> — CI gates, route discovery, Storybook, dark mode, baselines</summary>\n\n```bash\n# CI mode — non-zero exit on critical issues, SARIF output for code scanning.\nmotionlint review https://staging.acme.dev --ci --threshold critical --format sarif -o ux.sarif\n\n# Polished, shareable HTML review with embedded screenshots + before/after fixes.\nmotionlint review http://localhost:3000 --format html -o review.html\n\n# Review every route the site knows about (sitemap.xml + Next.js app/ directory).\nmotionlint review http://localhost:3000 --discover-routes\n\n# Storybook mode — discover stories from /index.json, review each story iframe as its own route.\nmotionlint review http://localhost:6006 --storybook\n\n# Color-scheme sweep — light and dark modes, plus Windows High Contrast.\nmotionlint review http://localhost:3000 --schemes --forced-colors --format html -o review.html\n\n# Interaction affordances — grid each element's default/hover/focus/active states.\nmotionlint review http://localhost:3000 --state-grid\n\n# Agent focus — keep only the top 5 findings, and only ones not seen in prior runs.\nmotionlint review http://localhost:3000 --max-findings 5 --new-only\n\n# Before/after comparison — PR preview vs. production baseline.\nmotionlint review https://pr-123.preview.example.com --against https://prod.example.com\n\n# Reviewer focus — cap the SARIF upload at 10 annotations per report.\nmotionlint review https://staging.acme.dev --format sarif -o ux.sarif --max-pr-annotations 10\n\n# Pick a provider explicitly (auto-detect picks the first reachable one).\nmotionlint review http://localhost:3000 --provider anthropic --model claude-sonnet-5\n\n# Track provider quality across runs + teach the reviewer from eval misses.\nmotionlint eval --provider anthropic --evolve\n```\n\n</details>\n\nPackage on npm: [motionlint](https://www.npmjs.com/package/motionlint).\n\nSample terminal output for a flow review:\n\n```text\n$ motionlint flow --spec flows/signup.json --provider anthropic\n→ Running flow \"signup-happy-path\" against http://localhost:3000/signup (11 steps, 50ms intervals × 750ms window)\n  provider: anthropic (claude-sonnet-5)\n  capturing flow…\n  ✓ step 1: 16 frames    ✓ step 2: 16 frames    ✓ step 3: 16 frames    …\n  captured 176 frames in 31s\n  contact sheet → .motionlint/flows/signup-happy-path-…png\n  analyzing flow…\n  report → .motionlint/flows/signup-happy-path.md\n\nScore: 4/10 · 3 critical findings\n  [critical] interaction — input focus rings missing across steps 2/4/6\n  [critical] interaction — submit button has no pressed state\n  [critical] loading_state — 1.4s wait with no spinner during submit\n```\n\n## Try the demo\n\nA multi-route TS animation showcase ships in [demo/](demo/) — 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:\n\n```bash\nnode demo/server.mjs                                # http://localhost:4173\nmotionlint review http://localhost:4173/cat --record --embed\nmotionlint flow --spec flows/signup.json\nmotionlint tune http://localhost:4173/dashboard\n```\n\nRoutes available: `/`, `/pricing`, `/signup`, `/dashboard`, `/loading`, `/cat`. Reports go to `.motionlint/reports/`, screenshots to `.motionlint/screenshots/`, videos to `.motionlint/videos/`.\n\n## Setup\n\n### API keys\n\nMotionLint auto-loads a `.env` file from the working directory at startup:\n\n```bash\n# .env (gitignored)\nANTHROPIC_API_KEY=sk-ant-...\n# or\nOPENAI_API_KEY=sk-...\n# or\nGOOGLE_API_KEY=...\n# or run a local Ollama (no key needed) — auto-detected on http://localhost:11434\n```\n\nReal environment variables take precedence over `.env`. With no key set and no Ollama running, MotionLint falls back to a deterministic **mock provider** so the full pipeline (capture → analysis → report) still runs end-to-end for smoke tests.\n\n### Provider auto-detect\n\nMotionLint 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>`. See [Providers in depth](#providers-in-depth) for the per-provider quality scorecard and how to pick.\n\n---\n\n> *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.*\n\n## Validated quality across providers\n\nThe flow-review pipeline was stress-tested across **12 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).\n\nRun on **2026-07-27** against the current flagship from each major provider:\n\n| Provider · model | Recall (broken caught) | FPR (clean flagged) | Score gap | Wall time |\n| --- | --- | --- | --- | --- |\n| **OpenAI · gpt-5.6-sol** | **100%** (12/12) | 0% (0/12) | +3.3 | 10.9 min |\n| **OpenAI · gpt-5.5** | **100%** (12/12) | 0% (0/12) | +3.3 | 11.5 min |\n| **Anthropic · claude-opus-5** | **100%** (12/12) | 8% (1/12) | +4.1 | 21.0 min |\n| **Google · gemini-3.6-flash** | **100%** (12/12) | 17% (2/12) | +5.1 | 4.9 min |\n| **Anthropic · claude-sonnet-5** | **100%** (12/12) | 33% (4/12) | +3.1 | 10.8 min |\n\n**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.\n\n**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.5` 1/12 → 0/12, `gpt-5.6-sol` 2/12 → 0/12, `gemini-3.6-flash` 3/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.\n\n<details>\n<summary>Why the older numbers in this table's history were wrong</summary>\n\nThe first 2026-07-27 run of this suite put `claude-opus-5` at 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.\n\nAnthropic's `max_tokens` defaulted to 4096. Verbose responses hit the ceiling mid-JSON, and the unparseable result was scored as `0/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.\n\nThe same bug deflated FPR everywhere: a truncated review reports *nothing*, 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.\n\n</details>\n\nFull per-provider scorecards in [.motionlint/stress/](.motionlint/stress/) after running [scripts/run-all-benchmarks.mjs](scripts/run-all-benchmarks.mjs). Use `--only <provider>:<model>` to re-run a single model.\n\n## Providers in depth\n\n| Provider | Model | Setup | Quality (24 fixtures) | Cost per review¹ |\n| --- | --- | --- | --- | --- |\n| `google` | `gemini-3.6-flash` | `GOOGLE_API_KEY=…` | 100% recall · 17% FPR · +5.1 gap | **$0.019** |\n| `anthropic` | `claude-sonnet-5` | `ANTHROPIC_API_KEY=…` | 100% recall · 33% FPR · +3.1 gap | $0.089 |\n| `openai` | `gpt-5.5` | `OPENAI_API_KEY=…` | 100% recall · 0% FPR · +3.3 gap | $0.248 |\n| `openai` | `gpt-5.6-sol` | `OPENAI_API_KEY=…` | 100% recall · 0% FPR · +3.3 gap | $0.265 |\n| `anthropic` | `claude-opus-5` | `ANTHROPIC_API_KEY=…` | 100% recall · 8% FPR · +4.1 gap | $0.293 |\n| `ollama` | any vision model | `ollama serve` + `ollama pull <model>` | not benchmarked in this run | $0 |\n| `mock` | heuristic stub | (auto fallback) | n/a — deterministic stub for CI smoke tests | $0 |\n\n¹ **Measured, not estimated** — one real `motionlint review` per 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 with `formatUsageLine()` 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 and `motionlint audit` make **zero** LLM calls and cost nothing.\n\n**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.\n\n### How to pick\n\n- **Default.** Google `gemini-3.6-flash` — 100% recall, **13× cheaper** than 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.\n- **Anthropic house.** `claude-sonnet-5` at $0.089 — 3× cheaper than `claude-opus-5` with identical recall. Opus 5 costs more and took **2× 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).\n- **OpenAI house.** `gpt-5.5` and `gpt-5.6-sol` are indistinguishable on every measured axis and within 7% on price. Take whichever your account already has.\n- **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.\n- **Local / air-gapped.** Any Ollama vision model works, but confirm it *is* vision-capable: some accept images over the API, silently ignore them, and answer from the prompt alone. None was benchmarked in this run.\n\n### Switching providers\n\nEvery command honours `--provider` and `--model`:\n\n```bash\nmotionlint review http://localhost:3000 --provider openai    --model gpt-5.5\nmotionlint flow   --spec flows/signup.json --provider google --model gemini-3.6-flash\nmotionlint review http://localhost:3000 --provider ollama    --model llava:13b\n```\n\n### Benchmarking your own provider\n\nTo compare a new provider against the same 24-fixture stress test:\n\n```bash\nnode -e \"\nimport('./dist/config/env.js').then(async ({ loadEnv }) => {\n  loadEnv();\n  const { runStress, renderStressMarkdown } = await import('./dist/flow/stress.js');\n  const { writeFile, mkdir } = await import('node:fs/promises');\n  const { resolve } = await import('node:path');\n  await mkdir('.motionlint/stress', { recursive: true });\n  const r = await runStress({\n    stressPath: resolve('eval/animation-stress.json'),\n    fixturesDir: resolve('eval/animation-fixtures'),\n    artifactDir: resolve('.motionlint/stress'),\n    provider: 'YOUR_PROVIDER',  // 'openai' | 'google' | 'ollama'\n  });\n  await writeFile('.motionlint/stress/SCORECARD.md', renderStressMarkdown(r), 'utf8');\n  console.error('Recall:', (r.broken_recall*100).toFixed(0)+'%, FPR:', (r.good_false_positive_rate*100).toFixed(0)+'%, gap:', r.avg_score_gap.toFixed(1));\n});\n\"\n```\n\nOpen `.motionlint/stress/SCORECARD.md` for the per-pattern breakdown.\n\n## How `motionlint flow` works\n\nStatic screenshots can't tell you whether a flow's animations and interaction states work — only whether the final frame looks right. `motionlint flow` fills that gap.\n\nGiven a scripted user journey, it:\n\n1. Runs the journey in headless Chromium via Playwright — clicking, typing, hovering, scrolling, pressing keys exactly like a user would.\n2. Captures a **burst of 16 frames over 750ms (50ms intervals) after every interaction** via CDP screencast (`Page.captureScreenshot` JPEG, ~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.\n3. Records the **full Playwright video** as an artifact you can scrub later.\n4. Composites every burst into a labeled **contact sheet** — one row per step, frames laid out in sub-rows.\n5. 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.\n6. 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.\n\n### Multi-animation handling\n\nA single recording can capture and analyze multiple concurrent animations. Validated on:\n\n- **Dashboard reveal** (3 concurrent: tile stagger + counter ramps + chart bar rise)\n- **Modal stack** (backdrop fade + modal slide+fade + inner content stagger)\n- **Rich form feedback** (focus ring + button press + loading spinner + success card)\n- **Scroll-driven** (scroll-progress bar + IntersectionObserver section reveal + parallax hero)\n\nThe LLM correctly identifies *which* animations are broken without false-flagging the working ones — see the validated-quality table.\n\n### Scroll-driven animations\n\nFor sites with scroll-linked animations, `scroll <px>` steps animate the scroll over the burst window via `requestAnimationFrame` so each frame shows progressive scroll position and the LLM sees the timing as the page scrolls.\n\n### Flow examples\n\n```bash\n# Inline DSL — semicolon-separated steps\nmotionlint flow \\\n  --url http://localhost:3000 \\\n  --steps \"navigate /signup; click input#email; type input#email=ada@example.com; click button[type=submit]; wait 2000; capture \\\"post-submit\\\"\" \\\n  --name signup-happy-path\n\n# Or load a structured spec with expected_animations[] hints\nmotionlint flow --spec flows/signup.json --provider anthropic\n\n# Pass team motion preferences (philosophy + inspirations + accepted defaults)\n# Embedded into the prompt AND the report's CC handoff block.\nmotionlint flow --spec flows/signup.json --preferences flows/preferences.md\n\n# Tighten the interval below 50ms for fine-grained timing review\nmotionlint flow --spec flows/signup.json --interval 30 --burst-ms 600\n\n# Auto-detect: scan the page's animations, pick an interval that captures\n# the shortest one with 4 frames inside it (clamped to [20, 100]ms).\nmotionlint flow --spec flows/signup.json --auto-interval\n```\n\n### Inline DSL reference\n\n| Action | Form | Notes |\n| --- | --- | --- |\n| navigate | `navigate /pricing` | path or full URL |\n| click | `click button#start` | CSS selector |\n| hover | `hover .feature` | CSS selector |\n| type | `type input#email=ada@example.com` | selector=value |\n| press | `press Enter` | keyboard key |\n| scroll | `scroll 800` | pixels; animates over the burst window |\n| wait | `wait 500` | ms |\n| capture | `capture \"post-submit\"` | take an explicit burst with optional label |\n\nDefaults: a frame burst is taken after *every* interaction. Pass `--no-implicit-bursts` to only burst on explicit `capture` steps. Pass `--no-record` to skip video.\n\nThree ready-to-run sample flows ship in the repo: [flows/signup.json](flows/signup.json), [flows/loading-state.json](flows/loading-state.json), and [flows/preferences.md](flows/preferences.md).\n\n## How the Animation Tuner works\n\nMost AI coding tools generate animations from scratch. The Tuner lets you **tune 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.\n\n<p align=\"center\">\n  <img src=\"docs/media/tuner.gif\" width=\"800\" alt=\"The Animation Tuner: replaying a detected animation, dragging its duration slider from 300ms to 150ms, then applying the ease-out (Emil) preset\">\n</p>\n\n```bash\nmotionlint tune http://localhost:3000 --open\n```\n\nThis:\n\n1. 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 `@keyframes` running on the page.\n2. Captures every detected animation: the element selector, source library, timing parameters, and bounding box.\n3. Generates a self-contained interactive HTML page at `.motionlint/tuner/index.html` (auto-opens with `--open`):\n   - **Live preview surface** per animation (Shadow DOM — no iframes, no flash, themed to the source page).\n   - **Sliders** for duration / delay / stagger / speed.\n   - **Easing-preset dropdown** — Emil Kowalski's strong curves lead (ease-out, ease-in-out, iOS drawer), then the softer/decorative options.\n   - **Inline standards linting** — each card flags where the animation deviates from the motion standards (severity badge, fix, suggested value), with a header score.\n   - **Comments box** per animation for design rationale.\n4. Exports a markdown file plus a Claude-Code-ready prompt with a structured `changes[]` JSON block. Paste that into CC and it edits your codebase to apply the new parameters.\n\n```text\n$ motionlint tune http://localhost:3000\n\n→ Capturing animations on http://localhost:3000…\n  detected 15 animation(s)\n  tuner → /Users/you/proj/.motionlint/tuner/index.html\n  open with: file:///Users/you/proj/.motionlint/tuner/index.html\n```\n\n## Animation standards — `motionlint audit`\n\nMotionLint encodes [Emil Kowalski's](https://emilkowal.ski/) design-engineering standards as a **deterministic linter** — no vision model, no API key, no cost. `motionlint audit` instruments the page, reads the real timing/easing/transform values every animation is running, and grades them:\n\n<p align=\"center\">\n  <img src=\"docs/media/audit-report.gif\" width=\"800\" alt=\"The audit HTML report: score ring, then scrolling through findings — each shows what's happening, why it matters, the fix, and current vs suggested easing curves drawn as graphs\">\n</p>\n\n| Category | What it catches | The standard |\n| --- | --- | --- |\n| **Easing** | `ease-in` on UI; weak built-in curves on deliberate entrances | Entering/exiting → strong ease-out `cubic-bezier(0.23, 1, 0.32, 1)`; never `ease-in` |\n| **Duration** | UI motion over the 300ms ceiling (modals/drawers get 200–500ms) | A 180ms transition feels snappier than a 400ms one; exits ~20% faster |\n| **Physicality** | `scale(0)` entrances | Nothing appears from nothing — start from `scale(0.95)` + `opacity: 0` |\n| **Performance** | `transition: all`, animating layout properties, stray infinite loops | Animate `transform` and `opacity` only — they skip layout/paint |\n| **Cohesion** | Hand-rolled easing-curve sprawl; stagger intervals outside the 30–80ms band | Curves and durations should live as shared tokens; grouped entrances stagger 30–80ms apart |\n| **Duration (pairs)** | Exits that aren't faster than their entrance (`fadeIn` 300ms / `fadeOut` 300ms) | Exits run ~20% faster than the matching entrance |\n\n```bash\nmotionlint audit http://localhost:3000 --open          # polished HTML report, scored 0–100\nmotionlint audit http://localhost:3000 --json audit.json --ci   # machine-readable; non-zero on critical\n```\n\nAdd `--layout` to also lint layout (tap targets, text size, contrast, overflow) from live DOM measurements — still deterministic, still no API key.\n\nAdd `--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+.\n\nThe report pairs every finding with a **before → after** panel; easing findings render a live cubic-bezier curve comparison so the fix is visible, not just described. The same standards feed the `flow` review prompt (so vision findings cite concrete rules) and appear inline in the Animation Tuner.\n\n## MCP server — tools, resources, deployment\n\nMotionLint ships an MCP server over stdio so an LLM agent can drive it directly inside a chat. The `motionlint mcp` subcommand boots it; the agent client spawns the process when a tool is called.\n\n### Installing in Claude Code\n\nPublished-npm version (recommended):\n\n```bash\nclaude mcp add motionlint -- npx -y motionlint mcp\n```\n\nLocal checkout (handy while developing):\n\n```bash\nclaude mcp add motionlint -- node /absolute/path/to/motionlint/dist/index.js mcp\n```\n\nAfter registration:\n\n1. Confirm it appears: `claude mcp list` — `motionlint` should show as `running` or `available`.\n2. Make sure API keys are reachable. The MCP server inherits the env it's spawned in. Cleanest path: drop a `.env` file in the project directory you're working from — MotionLint auto-loads it on startup.\n3. First run: `npx playwright install chromium` if you haven't already.\n\nThen in Claude Code:\n\n> *\"Use motionlint to review the local app at mobile and desktop and tell me the top 3 issues to fix.\"*\n>\n> *\"Run motionlint review_flow on `http://localhost:3000/signup` with steps `click input#email; type input#email=test@test.com; click button[type=submit]; wait 2000; capture` and check the animations.\"*\n>\n> *\"Run motionlint tune_animations on `http://localhost:3000/pricing` — I want to fine-tune the card hover animations.\"*\n\n### Tools exposed\n\n| Tool | What it does |\n| --- | --- |\n| `review_url(url, viewports?, provider?, model?, wait_for?, record?, format?, max_findings?, max_pr_annotations?, new_only?)` | Static UX review of a URL at multiple viewports. Returns a markdown / JSON / SARIF report. |\n| `review_routes(base_url, routes, viewports?, ..., max_findings?, max_pr_annotations?, new_only?)` | Same review across multiple routes of one app. |\n| `review_flow(url, steps?\\|spec_path?, preferences_path?, provider?, ...)` | Animation/interaction review of a scripted user journey. Returns a flow report with the structured CC handoff block. |\n| `tune_animations(url, viewport_*?, settle_ms?, output?)` | Detects every animation on a page and writes an interactive HTML tuner. Returns the file path. |\n| `get_latest_report(format?)` | Returns the most recent review/flow report content. |\n\nResources: `motionlint://reports/latest` — the most recent report content.\n\n### Deployment checklist\n\nBefore deploying or sharing the MCP server with other users:\n\n- [ ] **Build is fresh.** `npm run build` then verify `dist/index.js` exists. Without this, `motionlint mcp` won't start.\n- [ ] **Playwright Chromium installed** on 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 on `npm install`).\n- [ ] **API keys reachable** — either via shell env or via a `.env` file in the working directory the MCP client launches from.\n- [ ] **Smoke-test the MCP surface.** `npm test` includes an MCP smoke test that boots the server, lists tools, and asserts the expected tool surface.\n- [ ] **No secrets committed.** `.env` is gitignored; `.env.example` should be a placeholder. Worth a final `git diff --cached | grep -i 'sk-\\|api_key'` before pushing.\n- [ ] **Confirm with `claude mcp list`** that the server shows up and isn't erroring at startup.\n\n## CI integration\n\n```yaml\n# .github/workflows/ux.yml\n- run: npm ci\n- run: npx playwright install chromium\n- run: npx motionlint review $STAGING_URL --ci --threshold critical --format sarif -o ux.sarif\n- uses: github/codeql-action/upload-sarif@v3\n  with: { sarif_file: ux.sarif }\n```\n\nMotionLint exits with `1` when critical issues exceed the configured threshold (`failOnCritical`) — wire it as a status check.\n\n## What it captures · what it analyzes\n\n**Captures:**\n\n- **Full-page screenshots** at three default viewports (mobile 375 / tablet 768 / desktop 1440). Override via config.\n- **Above-the-fold** screenshots with `--no-full-page`.\n- **Videos** of the navigation+capture run with `--record` (Playwright `.webm`).\n- **Interaction sequences** before capture: `click`, `hover`, `type`, `scroll`, `wait`.\n- **Auth state**: cookies, `localStorage`, and a `beforeNavigate` script — all configurable in `.motionlintrc.json`.\n\n<p align=\"center\">\n  <img src=\"docs/media/flow-contact-sheet.png\" width=\"800\" alt=\"A flow burst-capture contact sheet: timestamped frames of the signup form animating, laid out in a grid — this is what the vision model reviews\">\n</p>\n<p align=\"center\"><sub>A <code>motionlint flow</code> contact sheet — timestamped bursts after each interaction, exactly what the vision model sees.</sub></p>\n\n**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:\n\n```json\n{\n  \"category\": \"hierarchy\",\n  \"severity\": \"critical | warning | suggestion\",\n  \"location\": \"above-the-fold hero\",\n  \"issue\": \"Primary CTA blends into the background gradient.\",\n  \"why_it_matters\": \"Users miss the conversion path on first scroll.\",\n  \"fix\": \"Increase background contrast or use a solid surface behind the button.\"\n}\n```\n\nOverride the prompt with `--rules path/to/your-design-rules.md` to inject project-specific heuristics.\n\nEvery review capture also takes a **DOM 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 are **drawn as severity-colored bounding boxes on the screenshot** in the HTML report (and reported as `Where: E3 at (x, y) w×h` in markdown). Refs the page never listed are dropped — the model can't annotate what it wasn't shown.\n\nWith `--format html` the findings render as a single shareable report — score ring, per-dimension breakdown, and an issue → fix panel per finding with the annotated screenshot:\n\n<p align=\"center\">\n  <img src=\"docs/media/review-report.gif\" width=\"720\" alt=\"The HTML review report: score ring animates in, then issue-to-fix panels with embedded screenshots scroll past\">\n</p>\n\n## Configuration reference\n\nDrop a `.motionlintrc.json` in your repo root (or use `motionlint.config.js` / a `\"motionlint\"` key in `package.json`):\n\n```json\n{\n  \"provider\": \"auto\",\n  \"fallbackProvider\": \"anthropic\",\n  \"fallbackModel\": \"claude-sonnet-5\",\n  \"viewports\": {\n    \"mobile\":  { \"width\": 375,  \"height\": 812 },\n    \"tablet\":  { \"width\": 768,  \"height\": 1024 },\n    \"desktop\": { \"width\": 1440, \"height\": 900 }\n  },\n  \"defaultViewports\": [\"mobile\", \"desktop\"],\n  \"waitFor\": \"networkidle\",\n  \"waitTimeout\": 10000,\n  \"screenshotDir\": \".motionlint/screenshots\",\n  \"videoDir\": \".motionlint/videos\",\n  \"reportDir\": \".motionlint/reports\",\n  \"rules\": null,\n  \"record\": false,\n  \"maxFindings\": null,\n  \"maxPrAnnotations\": null,\n  \"memory\": {\n    \"enabled\": true,\n    \"path\": \".motionlint/memory.json\",\n    \"baseline\": \".motionlintignore\",\n    \"newOnly\": false\n  },\n  \"resources\": { \"maxConcurrentReviews\": null, \"providerCallsPerMinute\": null, \"maxTokensPerRun\": null },\n  \"ci\": { \"threshold\": \"warning\", \"failOnCritical\": true },\n  \"auth\": { \"cookies\": null, \"localStorage\": null, \"beforeNavigate\": null }\n}\n```\n\n## Review volume control\n\nRe-running review on the same routes used to surface the same findings every run. Two mechanisms keep the output focused:\n\n- **Per-run output cap** — `--max-findings N` (or `maxFindings` in config) keeps only the top N findings per run, severity-ordered, so an agent works on what matters most first. The report's `Omitted` line says how many were capped.\n- **PR-surface cap** — `--max-pr-annotations N` (or `maxPrAnnotations` in config; SARIF only) emits at most N results per report, severity-ordered, so a code-scanning upload doesn't flood a PR with annotations. The dropped count lands in the SARIF run's `omitted_by_pr_cap` property.\n- **Resource cap** — `resources.maxConcurrentReviews` bounds how many reviews run at once in one process (an MCP server fielding several agents in flight), and `resources.providerCallsPerMinute` is a process-wide sliding-window ceiling on vision-LLM calls (provider quota / spend control; also applies to `flow` reviews, where each `--consistency` sample counts). Both default to unlimited; both are config-only. Note they compose: a review holding a concurrency slot also waits out the rate limiter, so tight values on both multiply latency.\n- **Cost ceiling** — every provider call's token usage is captured and totalled per run (a `Tokens:` line in reports, `token_usage` in SARIF run properties). `--max-tokens N` (or `resources.maxTokensPerRun` in config) sets a per-run token budget: once the running total crosses it, remaining viewports are skipped and the report lists them under `skipped_viewports`. Providers that report no usage still count calls but consume no budget.\n- **Cross-run memory** — every finding gets a stable id (hash of category + element location + normalized issue text). Recurrence detection goes further than exact hashing: category-synonym compatibility plus canonical-token overlap (thresholds calibrated on real cross-run data) matches the same fault even when the vision LLM rewords it between runs. Sightings are recorded per URL in `.motionlint/memory.json`; recurring findings are annotated with *seen in N prior runs* rather than silently dropped. Opt into deltas-only with `--new-only`. To permanently wave off a finding, copy its id into `.motionlintignore` (one hash per line, `#` comments and trailing notes allowed). Disable everything with `--no-memory`.\n\nSARIF output carries the finding id as a `partialFingerprint`, so GitHub code scanning dedups the same finding across runs and PRs natively.\n\nConcurrent reviews of the same project are safe: the memory store is updated under a stale-aware file lock (`memory.json.lock`), so parallel runs don't clobber each other's recorded sightings. A wedged lock never fails a review — after a short wait the run warns and proceeds without it.\n\n## Use cases\n\n- **Pre-merge UX guardrail.** Solo dev or 2-person startup with no designer. Run `motionlint review https://pr-123.preview.example.com --ci --threshold critical` in CI; warning-or-worse blocks the merge until you've at least seen the issues.\n- **MCP design colleague inside Claude Code.** Add MotionLint as an MCP server, then ask CC: *\"review the local app at mobile and desktop and tell me the top 3 issues to fix.\"* CC drives the tool and gets back annotated feedback in the same conversation.\n- **Continuous quality monitoring.** Schedule a nightly cron (`motionlint review https://prod.example.com --format sarif -o ux.sarif`) and surface SARIF in your code-scanning dashboard so production regressions get caught the morning after.\n- **Animation / flow QA on a feature you just shipped.** `motionlint flow` runs a scripted user journey through Playwright like a human would, captures frame bursts at every interaction, records video, and asks the LLM to review the *animation behavior* across the captured frames.\n- **Live animation tuning + handoff to Claude Code.** Capture every animation on a page, tune timing/easing/delay live with sliders, export a structured prompt CC can act on directly.\n\n## Project layout\n\n```text\nsrc/\n  capture/      Playwright capture (screenshot, mosaic, DOM snapshot) + interaction sequences\n  providers/    Vision LLM providers (ollama, anthropic, openai, google, mock) + self-consistency wrapper\n  analysis/     Rubric-style UX prompt + JSON parser + rule injection\n  report/       Markdown / JSON / SARIF report generators\n  eval/         Tiered eval harness (L1/L2/L3 fixtures, scorer, runner, report)\n  flow/         Flow runner — spec parser, capture orchestrator, animation-aware report\n  tuner/        Animation Tuner — extractor, instrumentation script, Shadow-DOM render\n  mcp/          MCP server for Claude Code\n  cli/          Commander.js commands + terminal output\n  config/       cosmiconfig loader + .env loader\ndemo/           TS animation showcase used as a review target\nflows/          Sample flow specs (signup, loading-state) for `motionlint flow`\neval/fixtures/  Labelled HTML pages with seeded UX faults at three complexity levels\ntest/           Node test runner unit + integration tests\n```\n\n## Roadmap\n\n**v0.1 (this release)** — shipped:\n\n- Three CLI commands: `review`, `flow`, `tune` + MCP server (`motionlint mcp`).\n- Five vision providers: Anthropic, OpenAI, Google, Ollama, mock.\n- Multi-viewport static review with mosaic capture, DOM measurement side-channel, self-consistency sampling, soft-keyword scoring with synonym graph.\n- Tiered eval harness (L1 / L2 / L3) with 21 labelled fixtures and structured `next_actions[]` JSON for downstream LLM coding tools.\n- Flow review at **50ms inter-frame intervals** via CDP screencast (16 frames × 750ms burst), with multi-animation and scroll-driven support.\n- Animation Tuner with Shadow-DOM previews, live sliders, easing presets, Claude-Code export.\n- Animation stress-test harness validated at **100% recall / 0% FPR on 24 fixtures across 12 patterns**.\n- Team motion preferences markdown (`--preferences`) embedded into the LLM rubric and the CC handoff block.\n- Auto-interval scan (`--auto-interval`) that picks an inter-frame interval based on the shortest animation detected on the page.\n- SARIF output for GitHub code scanning.\n\n**v0.2 (in progress)** — shipped so far:\n\n- Token accounting + per-run cost ceiling (`--max-tokens` / `resources.maxTokensPerRun`; `Tokens:` line in every report).\n- Auto-discover routes (`--discover-routes`: sitemap.xml + Next.js app directory).\n- Annotated bounding boxes: DOM element refs in the prompt, findings drawn on the screenshot in the HTML report.\n- Interaction-state grids (`--state-grid`: default/hover/focus/active per element, one labeled image).\n- Provider scorecard history with per-model regression detection (`.motionlint/eval-history.json`).\n- Closed-loop prompt evolution from eval `next_actions` (`eval --evolve` → learned heuristics in review prompts).\n- Two new audit rules: stagger-interval band (30–80ms) and exit-~20%-faster-than-entrance.\n\n**v0.2 (next)**:\n\n- GitHub Action wrapper (`motionlint-action`).\n\n## Acknowledgments\n\nMotionLint stands on other people's work:\n\n- **[Emil Kowalski](https://emilkowal.ski/)** — the animation standards behind `motionlint audit`, the tuner's easing presets, and the flow-review rubric are distilled from his design-engineering writing and his [animations.dev](https://animations.dev/) course. His open-source UI libraries — [sonner](https://github.com/emilkowalski/sonner) (toasts) and [vaul](https://github.com/emilkowalski/vaul) (drawers) — are living reference implementations of the motion these rules describe. MotionLint is an independent project, not affiliated with or endorsed by Emil.\n- **[ctx](https://github.com/ctxrs/ctx)** ([ctx.rs](https://ctx.rs)) — local coding-agent history search. We used it while developing the cross-run memory layer to study how findings survive (or vanish) across agent runs; those experiments directly shaped the finding-id and baseline design.\n\n## License\n\n[MIT](LICENSE) © Resila Technologies Inc.\n",
  "bytes": 39955,
  "sha": "1a83d18690d2af3a251723198061e8bdbc6dcfaa2ab303ec31e40c4d0c5c5c8d",
  "repo_slug": "bobaba99/motionlint",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_bobaba99_motionlint_6f0816ab/readme"
}