Back to Motion

Plan pr-3749: Land "Replace VisualElement value management and renderer with effects"

plans/issues/pr-3749.md

12.41.09.3 KB
Original Source

Plan pr-3749: Land "Replace VisualElement value management and renderer with effects"

Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving on. If anything in "STOP conditions" occurs, stop and report — do not improvise. When done, update this plan's row in plans/issues/README.md.

Drift check (run first): gh pr view 3749 --json state,headRefOid,mergeStateStatus Expected: state: OPEN, headRefOid: 80d85dbeb2a9bc0391b72f9da2b6ada6b3b8eac1. If closed/merged or head differs, STOP.

Status

  • Priority: P1 (strategic: plans 004 + the MotionNode arc are sequenced behind it)
  • Effort: M
  • Risk: HIGH (rewrites the render pipeline of every motion component; removes public motion-dom exports)
  • Depends on: land AFTER plans/issues/pr-3748.md (no git conflict, but the animate-layout HTML suite must re-validate the combination — see Step 4)
  • Category: tech-debt / architecture
  • Planned at: commit 42bfbe3ed, 2026-06-11
  • PR: https://github.com/motiondivision/motion/pull/3749 (branch worktree-style-effect)

Why this matters

This unifies the two render pipelines: VisualElement stops using build()/renderState/renderInstance() and instead binds values through the effects system (MotionValueState with an ordered slot/contributor model). It is the substrate the repo's roadmap is sequenced behind: plans/README.md:33,37 defer both the WAAPI transform-shorthand implementation (plan 004) and the framework-agnostic MotionNode/vanilla-drag arc until "the effects/VisualElement unification (worktree-style-effect) direction is settled". It also resolves the effects composition debt (the pathRotation 3-render-site hack) via the contributor/slot model. CI is fully green; what remains is review-thread disposition and a release-compat check.

Current state

  • 33 files (+1164/−669), 3 commits: (1) slot/contributor model on MotionValueState, (2) remove() semantics, (3) the pipeline swap.
  • Key mechanism — ordered contributor chains (packages/motion-dom/src/effects/MotionValueState.ts):
    ts
    contribute(name: string, index: number, builder: SlotBuilder) {
        ...
        chain[index] = builder
        this.get(name)?.dirty()
        return () => { chain[index] = undefined; this.get(name)?.dirty() }
    }
    
  • Central bind decision (packages/motion-dom/src/render/VisualElement.ts): granular binding for normal elements, full-render path for projection-driven ones, with a re-bind loop in update() when an element becomes projection-driven after mount (lazy-loaded layout features). isProjectionDriven() is gated on options.layout || options.layoutId || options.alwaysMeasureLayout — NOT on node existence (every element gets a projection node once layout features load; see memory "Effects/VisualElement unification" invariants).
  • Public API removals (PR body flags "Framer compat check before release"): motion-dom exports buildHTMLStyles, buildSVGAttrs, buildSVGPath, renderHTML, renderSVG removed (replaced by buildStyles, buildSVGProps); VisualElement.build/renderInstance/triggerBuild/removeValueFromRenderState removed.
  • Observable behavior shifts (encoded in the PR's own test changes): SVG values render as styles where supported (cypress/integration/svg.ts now asserts getComputedStyle($circle).fill, not the attribute); each bound value now has two change subscriptions (animate-prop.test.tsx 1 → 2).
  • Bundle: −0.09 kB size-rollup-m, +0.92 kB size-rollup-motion.
  • CI: ALL GREEN (CircleCI 17509–17513). Greptile review (3/5) left 3 unanswered threads (see Step 1). No human review.

Commands you will need

PurposeCommandExpected
Checkoutgh pr checkout 3749branch worktree-style-effect
Rebase (after 3748 lands)git fetch origin main && git rebase origin/mainno conflicts expected (zero file overlap with 3748)
Buildyarn build (repo root)exit 0
Jestyarn testpass (modulo known SSR TextEncoder / use-velocity failures)
HTML E2E incl. animate-layout + projectionthe repo's test-html CI job locally, or make test-e2e HTML portion159/159 per PR body, incl. 24 animate-layout + 108 projection
CIgit push --force-with-lease && gh pr checks 3749 --watchgreen

Scope

In scope: disposing the 3 Greptile threads (one may need a small code change, Step 1), rebase over #3748, the Framer-compat checklist, merge.

Out of scope: new features on the effects system; restoring the removed exports wholesale (the removal is the point — unless the compat check in Step 3 forces a deprecation shim, which is a maintainer decision); touching LayoutAnimationBuilder.ts.

Steps

Step 1: Dispose the three Greptile threads

  1. SVG projection (substantive): SVGVisualElement.renderValues ignores the _projection param; the old renderSVG forwarded it to applyProjectionStyles. Mitigating fact verified on main: SVGVisualElement.measureInstanceViewportBox = createBox (always a zero box), so SVG layout projection was already inert. Resolution options: (a) reply documenting "SVG projection was never functional; intentionally dropped" — needs maintainer sign-off, or (b) one-line defensive forward of the projection param matching the HTML path. If the operator marked this plan APPROVED in plans/issues/README.md, take (a) by posting the reply; otherwise mark the row BLOCKED ("SVG-projection thread needs maintainer disposition").
  2. Subscription-leak claim — REFUTED: bindToMotionValue begins with if (this.valueSubscriptions.has(key)) { this.valueSubscriptions.get(key)!() } on both main and the branch, so lazy-load re-binding cleans up. Reply on the thread with that line reference to close it.
  3. Inverted comment in packages/motion-dom/src/effects/style/index.ts (says "HTML element" where the guard is !isHTML): fix the comment text (one line) and push.

Verify: all three threads have replies; any code change is committed and CI re-green.

Step 2: Rebase after #3748 merges

Land order is fixed: #3748 first (see plans/issues/pr-3748.md). After it merges, rebase this branch onto main. No file overlap exists (#3748's only source file is LayoutAnimationBuilder.ts, untouched here), so expect a clean rebase.

Verify: rebase clean; yarn build exit 0.

Step 3: Framer compat check on removed exports (release gate)

The PR removes motion-dom exports (buildHTMLStyles, buildSVGAttrs, buildSVGPath, renderHTML, renderSVG) and VisualElement methods. Before merge, produce the evidence for the maintainer: search the Framer codebase / motion-plus (/Users/matt/Sites/plus) for usages: grep -rn "buildHTMLStyles\|buildSVGAttrs\|buildSVGPath\|renderHTML\|renderSVG\|triggerBuild\|removeValueFromRenderState" /Users/matt/Sites/plus --include=*.ts*. If you cannot access the Framer app codebase itself, say so explicitly in the README row — absence of motion-plus usage is necessary but not sufficient. Record findings as a PR comment.

Verify: comment posted listing each removed symbol and its found/not-found status, with the searched codebases named.

Step 4: Re-run the full HTML suite on the rebased combination, then merge

The animate-layout fixtures exercise #3748's engine rendering through #3749's new pipeline — the combination that has never run in CI before the rebase. Run the full HTML E2E suite locally (expect 159/159 + the fixtures #3748 added, incl. fire-and-forget). Push; wait for all checks green; gh pr merge 3749 --squash. Do NOT use gh pr edit.

Verify: gh pr view 3749 --json stateMERGED.

Done criteria

  • All 3 Greptile threads replied/resolved; comment fix pushed
  • Rebased on top of merged #3748; clean build
  • Compat-check comment posted (every removed symbol accounted for)
  • Full HTML suite green on the rebased branch locally AND in CI
  • PR merged; plans/issues/README.md row updated

STOP conditions

  • Rebase over #3748 produces conflicts (shouldn't — zero overlap at planning time; if it happens the assumption is stale, report).
  • Any animate-layout or projection HTML fixture fails on the rebased combination — this is exactly the cross-PR regression this plan exists to catch. Report fixture name + output; do NOT patch fixtures.
  • The compat grep finds a real consumer of a removed export — maintainer decision (deprecation shim vs consumer migration); report, don't choose.
  • The SVG-projection disposition (Step 1.1) is unresolved and the README row isn't APPROVED.

Maintenance notes

  • After this lands, the deferred roadmap items unblock: plan 004's WAAPI transform-shorthand implementation, the framework-agnostic MotionNode arc, and the layoutCurve revival (all referenced in plans/README.md:33,37-38). Also revisit memory note "Effects composition debt" — the slot model this PR ships is the "proper fix" that note describes; pathRotation should migrate to a contributor slot as follow-up.
  • Critical invariants for future changes (from the development of this branch): no bind-time renders; projection-driven = full renders; every element gets a projection node when layout features load.
  • Release notes must mention the removed motion-dom exports and the SVG fill-as-style behavior change.