plans/003-color-waapi-acceleration.md
Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the "STOP conditions" section occurs, stop and report — do not improvise. When done, update the status row for this plan in
plans/README.md— unless a reviewer dispatched you and told you they maintain the index.Drift check (run first):
git diff --stat 42bfbe3ed..HEAD -- packages/motion-dom/src/animation/waapi/If any in-scope file changed since this plan was written, compare the "Current state" excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition.
42bfbe3ed, 2026-06-10Color animations (backgroundColor, color, etc.) are deliberately excluded from WAAPI acceleration — a code comment defers them "until we implement support for linear() easing". That blocker is resolved: Motion now generates linear() easing strings for unsupported easings/springs (used by the existing WAAPI path). Plain color animations therefore fall to the per-frame JS animation path unnecessarily, re-rendering the element's styles every frame on the main thread. The repo's own PERFORMANCE_AUDIT.md ranks this MEDIUM impact / MEDIUM effort ("Re-enable (offload, not free render); fix backgroundColor naming"). Note the honest framing: color is paint-bound, so this is a main-thread offload, not free compositor rendering.
packages/motion-dom/src/animation/waapi/utils/accelerated-values.ts (complete file):
/**
* A list of values that can be hardware-accelerated.
*/
export const acceleratedValues = new Set<string>([
"opacity",
"clipPath",
"filter",
"transform",
// TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
// or until we implement support for linear() easing.
// "background-color"
])
⚠️ Naming trap: the commented entry is dash-case ("background-color"), but the name checked against this set is the Motion value name, which is camelCase ("backgroundColor"). Even uncommented as-is it would never match. Use camelCase names.
packages/motion-dom/src/animation/waapi/supports/waapi.ts:53-74 — the eligibility gate supportsBrowserAnimation():
return (
supportsWaapi() &&
name &&
(acceleratedValues.has(name) ||
(colorProperties.has(name) &&
hasBrowserOnlyColors(keyframes))) &&
(name !== "transform" || !transformTemplate) &&
!onUpdate &&
!repeatDelay &&
repeatType !== "mirror" &&
damping !== 0 &&
type !== "inertia"
)
Key fact: a colorProperties set already exists and colors ALREADY go through WAAPI when keyframes contain browser-only color formats (oklch/lab/etc.) — so the WAAPI color pipeline (keyframe serialization, easing conversion, interrupt sampling) is already exercised in production. This plan widens that gate from "browser-only colors" to "all animatable colors".
packages/motion-dom/src/animation/AsyncMotionValueAnimation.ts:173 — call site of supportsBrowserAnimation(resolvedOptions); the decision happens after keyframe resolution.
Chromium issue 41491098 referenced in the TODO: before changing code, check its current status (Step 1).
Repo conventions: this is a size-sensitive library; prefer minimal diffs. Cypress E2E pattern notes (from CLAUDE.md): use .then() not .should() for mid-animation measurements; long duration + linear easing + mid-animation computed-style check for target-value bugs; getAnimations() is only reliable for compositor properties in Electron — for colors, assert acceleration in Playwright/real Chromium instead, or assert behavior (computed style) in Cypress.
| Purpose | Command (from repo root unless noted) | Expected on success |
|---|---|---|
| Build | yarn build | exit 0 |
| motion-dom unit tests | npx jest --config packages/motion-dom/jest.config.json | no new failures |
| framer-motion client tests | cd packages/framer-motion && yarn test-client | no new failures |
| Playwright | npx playwright test tests/animate/ | all pass |
| Cypress React 18/19 | see CLAUDE.md "Running Cypress tests locally" (Vite directly per React version, foreground) | both pass |
| Lint | yarn lint | exit 0 |
In scope:
packages/motion-dom/src/animation/waapi/utils/accelerated-values.tspackages/motion-dom/src/animation/waapi/supports/waapi.ts (only if gating needs adjustment beyond the set)packages/motion-dom/src/animation/waapi/**/__tests__/dev/html/public/playwright/, tests/animate/)dev/react/src/tests/, packages/framer-motion/cypress/integration/) if behavior assertions are needed beyond PlaywrightOut of scope:
hasBrowserOnlyColors / colorProperties definitions — widening the gate must not change the browser-only-color fallback semantics (JS path CANNOT parse those formats; that branch is correctness, not perf).x/scale/rotate) — that is plan 004's design spike; do not attempt it here.NativeAnimation / keyframe serialization internals — they already handle colors via the browser-only-colors path; if they need changes, STOP.advisor/003-color-waapilinear() easing generation exists and is used by the WAAPI path: grep -rn "generateLinearEasing" packages/motion-dom/src --include="*.ts" | grep -v __tests__ → expect hits in the WAAPI easing pipeline.plans/003-notes.md.Verify: plans/003-notes.md exists with both answers. If linear() is NOT wired into the WAAPI easing path, STOP.
Add camelCase color names to acceleratedValues, replacing the commented dash-case entry. Start with the two most common: "backgroundColor" and "color". Do NOT add the whole colorProperties set in this pass (borderColor variants, fill/stroke for SVG have extra constraints — SVG is excluded by the HTMLElement instance check anyway, but keep the diff minimal and observable).
Keep the comment, updated to describe the remaining exclusions and why.
Verify: yarn build → exit 0.
Find the existing tests for supportsBrowserAnimation (grep -rn "supportsBrowserAnimation" packages/motion-dom/src --include="*.test.ts"); extend or create a test file asserting:
backgroundColor with standard hex/rgba keyframes → eligible (in a mocked-supporting environment, matching how existing tests mock supportsWaapi).backgroundColor with onUpdate present → not eligible (existing kill-switches still apply).borderTopColor) with standard keyframes → not eligible; with oklch keyframes → eligible (browser-only path unchanged).Verify: npx jest --config packages/motion-dom/jest.config.json --testPathPattern="waapi" → all pass.
Playwright (real Chromium): fixture animating backgroundColor red→blue, duration 10s, linear ease; assert (a) element.getAnimations().length > 0 (it is now a WAAPI animation), and (b) mid-animation computed background-color is strictly between the endpoints. Add an interrupt case: start a second animate() to a new color mid-flight, assert no jump (final value correct, no console errors) — interrupt sampling reads the WAAPI value, which is the riskiest behavior change.
Verify: npx playwright test tests/animate/ → all pass.
The blast radius is every color animation in the suite. Run, in order: motion-dom jest, framer-motion test-client, the full Cypress integration suite on React 18 AND React 19 (per CLAUDE.md local-run instructions), Playwright.
Verify: no new failures versus a baseline run on main (if any suite fails, first confirm whether it fails on main too before attributing it to this change).
grep -n "backgroundColor" packages/motion-dom/src/animation/waapi/utils/accelerated-values.ts → 1 match (uncommented)grep -n "background-color" packages/motion-dom/src/animation/waapi/utils/accelerated-values.ts → no uncommented matchesnpx jest --config packages/motion-dom/jest.config.json --testPathPattern="waapi" → all passnpx playwright test tests/animate/ → all pass including new interrupt caseplans/003-notes.md records linear()-wiring evidence and Chromium-issue statusgit status)plans/README.md status row updatedStop and report back (do not improvise) if:
linear() easing is not actually wired into WAAPI easing conversion — the TODO's stated blocker still holds.NativeAnimation, which is out of scope.type: "spring" on backgroundColor once manually): if spring→linear() conversion produces wrong colors, stop.linear() easing (noted in PERFORMANCE_AUDIT.md) — colors with spring easings will run WAAPI-on-main-thread there; acceptable (parity with today) but worth a release-notes line.fill/stroke) should reuse the Step 3 eligibility matrix; SVG is currently excluded wholesale by the subject instanceof HTMLElement check in waapi.ts:47.hasBrowserOnlyColors branch semantics, and the updated comment accurately states why remaining colors are excluded.