This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install dependencies (use make bootstrap for first-time setup)
make bootstrap
# Build all packages (ALWAYS run from repo root, not from individual packages)
yarn build # or: make build
# Watch mode for development
yarn watch # or: make watch
# Run tests
yarn test # Unit tests (Jest) for all packages
yarn test-playwright # Playwright E2E tests
# Run tests for a specific package
cd packages/framer-motion && yarn test-client # Client-side Jest tests
cd packages/framer-motion && yarn test-server # SSR Jest tests
# Lint
yarn lint # or: make lint
# Run E2E tests
make test-e2e # Runs all E2E tests (Next.js, HTML, React, React 19, Playwright)
make test-single # Run a single Cypress test (edit spec path in Makefile)This is a Yarn workspaces monorepo managed by Turborepo and Lerna.
- motion - Main public package (
npm install motion). Re-exports from framer-motion with cleaner entry points (motion/react,motion/mini) - framer-motion - Core implementation for React. Contains all animation logic, components, hooks, and features
- motion-dom - DOM-specific animation utilities (animate, scroll, gestures, effects). Framework-agnostic
- motion-utils - Shared utilities (easing, math helpers, error handling). No dependencies
- dev/react - React 18 development/test app (port 9990)
- dev/react-19 - React 19 development/test app (port 9991)
- dev/next - Next.js development/test app (port 3000)
- dev/html - Vanilla JS/HTML test pages (port 8000)
motion-utils (base utilities)
↓
motion-dom (DOM animation engine)
↓
framer-motion (React integration)
↓
motion (public API)
- animation/ - Animation system (animators, sequences, optimized-appear)
- components/ - React components (AnimatePresence, LayoutGroup, LazyMotion, Reorder)
- context/ - React contexts (MotionContext, PresenceContext, LayoutGroupContext)
- gestures/ - Gesture handling (drag, pan, tap, hover, focus)
- motion/ - Core motion component and feature system
- projection/ - Layout animation projection system (FLIP animations)
- render/ - Rendering pipeline (HTML, SVG, DOM utilities)
- value/ - Motion values and hooks (useMotionValue, useSpring, useScroll, useTransform)
IMPORTANT: Always write tests for every bug fix AND every new feature. Write a failing test FIRST before implementing, to ensure the issue is reproducible and the fix is verified.
"Failing test" means a test that reproduces the reported bug. The test should assert the expected behavior and fail because of the bug — not because your planned code doesn't exist yet. A TypeScript compile error for an API you're about to add is not a failing test. Write the test against the existing codebase, see it fail for the right reason, then implement the fix.
- Unit tests (Jest): For pure logic, value transformations, utilities. Located in
__tests__/directories alongside source. JSDOM does not support WAAPI (Element.animate()), so Jest tests only cover the JS animation fallback path. - E2E tests (Cypress): For UI behavior that involves DOM rendering, scroll interactions, gesture handling, or WAAPI animations. Test specs in
packages/framer-motion/cypress/integration/, test pages indev/react/src/tests/. - E2E tests (Playwright): For cross-browser testing and HTML/vanilla JS tests. Specs in
tests/, test pages indev/html/public/playwright/.
If a bug is reported with a reproduction but your unit test passes, do NOT conclude "already works." JSDOM lacks WAAPI, real layout, and real browser rendering. The bug is likely in the browser code path. You MUST:
- Create a Cypress E2E test that matches the reporter's reproduction as closely as possible.
- Verify the Cypress test fails before implementing a fix.
- If the bug involves
opacity,transform, React.lazy/Suspense, scroll, layout animations, or any visual behavior — start with Cypress, skip the unit test entirely.
- Create a test page in
dev/react/src/tests/<test-name>.tsxexporting a namedAppcomponent. It's automatically available at?test=<test-name>. - Create a spec in
packages/framer-motion/cypress/integration/<test-name>.ts. - Verify WAAPI acceleration using
element.getAnimations()in Cypress callbacks — but only for compositor properties (opacity, transform).getAnimations()won't return WAAPI animations for non-compositor properties like height/width in Electron. Don't use it for those.
- Use
.then(), not.should(), for mid-animation measurements.cy.should()retries assertions until they pass or timeout — so it will keep retrying until the animation completes, masking bugs where the target value is wrong..then()captures the value at a single point in time. - For animation target bugs, use long duration + linear easing + mid-animation measurement. Set
transition={{ type: "tween", ease: "linear", duration: 10 }}, wait 5s (50% through), then check the computed style with.then(). If the target is wrong, the value will be proportionally wrong and easy to detect. - Don't try
getAnimations()for non-compositor properties (height, width, etc.) in Cypress/Electron. It likely won't have WAAPI animations to inspect. Stick to computed style checks for these. - Don't use
onUpdatefor mid-animation pixel values. For keyword targets like"auto",onUpdatereports the keyword, not the resolved pixel value. UsegetComputedStyle()instead. - Always run Cypress tests in the foreground. Background bash commands hang silently and produce empty output, making debugging impossible. Cypress needs reliable stdout/stderr.
You MUST run every new Cypress test against both React 18 and React 19 before creating a PR. CI runs both and will break if you skip this.
Start the Vite dev server directly — do NOT use yarn start-server-and-test with yarn dev-server (turbo). Turbo starts ALL dev servers including Next.js, which is slow and unreliable. Starting Vite directly is instant:
# React 18 — start Vite directly, then run Cypress
PORT=$((10000 + RANDOM % 50000))
cd dev/react && TEST_PORT=$PORT yarn vite --port $PORT &
DEV_PID=$!
# Wait for server to be ready
npx wait-on http://localhost:$PORT
cd packages/framer-motion && cypress run --headed --config baseUrl=http://localhost:$PORT --spec cypress/integration/<test-name>.ts
kill $DEV_PID
# React 19 — same pattern, start its Vite server independently
PORT=$((10000 + RANDOM % 50000))
cd dev/react-19 && TEST_PORT=$PORT yarn vite --port $PORT &
DEV_PID=$!
npx wait-on http://localhost:$PORT
cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --config baseUrl=http://localhost:$PORT --headed --spec cypress/integration/<test-name>.ts
kill $DEV_PIDDo NOT set TEST_PORT globally with turbo — it affects both React 18 and 19 dev servers, causing port conflicts. Start each server independently on its own port as shown above.
Both must pass. If a test fails on one React version but not the other, investigate — do not skip it.
When waiting for the next frame in async tests:
async function nextFrame() {
return new Promise<void>((resolve) => {
frame.postRender(() => resolve())
})
}- Prioritise small file size — this is a library shipped to end users. Prefer concise patterns that minimise output bytes.
- Prefer optional chaining (
value?.jump()) over explicitifstatements - Use
interfacefor type definitions (enforced by ESLint) - No default exports (use named exports)
- Prefer arrow callbacks
- Use strict equality (
===) - No
vardeclarations (useconst/let)
When working on a bug fix from a GitHub issue:
- Read the issue first. Run
gh issue view <number>before doing anything else. Do not infer the ask from code context or agent exploration — read the actual issue to understand what's being requested. - Check git history early. Before tracing code, run
git log --grep="<keyword>" -- <relevant-file>to see if the bug was already fixed or if prior commits reveal the root cause. This can save an entire session of manual code tracing. - The reporter's reproduction code is the basis for your test. If the issue links to a CodeSandbox/StackBlitz, fetch it. Try multiple URL patterns if the first fails. If the issue has inline code, use that directly.
- If you cannot get the reproduction code, STOP and ask for help. Do not guess at what the reporter meant — that leads to tests that prove nothing. Message the team lead with the URL and ask them to provide the code.
- Do not proceed to a fix without a test that fails for the right reason. See the "Writing Tests" section above.
- Run one clean install, then wait for it to finish. Do not run
make bootstrap,yarn install, orcorepack enable && yarn installas overlapping background tasks — they interfere with each other. One install command, foreground, wait for completion.
- Get to a test fast. Do not spend extended time on static code analysis trying to find the bug by reading code. Read enough to form an initial theory (~5 min of tracing), then write a test and start experimenting. Most bugs are found faster through testing than through code reading. This is the single most common mistake — it has caused excessive time waste in multiple sessions.
- Use targeted searches, not broad exploration. Prefer
grep/globfor specific functions (e.g.isHTMLElement,supportsBrowserAnimation) over large Explore agent calls. Two targeted searches beat one broad sweep. - Pivot quickly when your theory is wrong. If tracing a code path (e.g. the render pipeline) is inconclusive after 2-3 rounds of investigation, step back and look at adjacent systems — utility functions, type guards, environment checks. The bug is often one level removed from where you expect it.
- When a bug can't be reproduced in the test environment, stop after 2-3 attempts. Electron/JSDOM differ from Chrome in significant ways (e.g.
offsetHeighton SVGElement, WAAPI support, React reconciliation in dev mode). If your test passes from the start: (1) do a web search for the behavioral difference between environments, (2) if the fix is clearly correct and defensive, apply it and write a test that validates the desired behavior — do not spend 10 Cypress runs trying to force a local failure. A test that can't fail without the fix is acceptable when the bug is environment-specific; note this in the PR. - Capture Cypress output on the first run. Use
tail -60on the output. Do not re-run Cypress with different grep patterns to capture errors — it wastes time and the information is in the first run. - Think defensively, not forensically. You don't always need to trace the exact scenario that produces bad input. If a function can receive invalid values and pass them to a browser API, the fix is to guard against that — regardless of which upstream path produces those values. Ask: "should this value ever reach this API?" If no, add the guard and move on.
- Choose the right test layer. Some behaviors can't be directly asserted in tests (e.g. Chrome WAAPI console warnings can't be intercepted via
console.warnspy). In these cases, unit-test the underlying logic (e.g.canAnimate,isAnimatable) rather than writing an E2E test that can't actually prove the bug is fixed. Write the E2E test too if it adds value, but recognize which test is the real regression gate. - Avoid background task sprawl. Do not launch multiple background exploration tasks early in a session. They complete after they're needed and generate noise. Launch tasks when you need their results, not speculatively.
gh pr edit fails on this repo due to GitHub's Projects Classic deprecation blocking the GraphQL mutation. This is expected — do not investigate, retry, or work around it. If gh pr create succeeded and the code is pushed, you're done. Move on.
Be thorough - I am at risk of losing my job.
Use time.now() from motion-dom/src/frameloop/sync-time.ts instead of performance.now() for frame-synced timestamps. This ensures consistent time measurements within synchronous contexts and proper sync with the animation frame loop.