plans/issues/issue-2601.md
initialExecutor instructions: Follow step by step; run the drift check first. Update the status row for this plan in
plans/issues/README.mdwhen done.Drift check (run first):
gh api repos/motiondivision/motion/issues/2601 --jq .state→ expect"open". If already closed, mark this plan DONE and stop. Confirm the excerpt below still matchespackages/framer-motion/src/motion/utils/use-visual-element.ts.
42bfbe3ed, 2026-06-11The issue (2024-03-29, 0 comments) bundles two things:
LazyMotion features,
<m.div initial={{opacity: 0}} animate={{opacity: 1}}>Hello</m.div> stayed
at opacity: 0 — "there's no Hello printed" — i.e. the animation did not
run when features resolved. The reporter's CodeSandbox (32vhxp) is behind
Cloudflare and could not be fetched, but the inline description is complete.initial styles at all until features load
(FCP concern), and skip initial if the element mounted before features
resolved.Item 1 was addressed by commit d8b7f5f2e ("Fix LazyMotion animation not
firing when state changes before features load", fixes #2759, released
v12.28.2, 2026-01). It tracks whether the component committed before the
VisualElement existed and forces the mount animation:
packages/framer-motion/src/motion/utils/use-visual-element.ts:84-86:
if (hasMountedOnce.current && visualElementRef.current) {
visualElementRef.current.manuallyAnimateOnMount = true
}
Existing coverage: dev/react/src/tests/lazy-motion-fast-state.tsx +
packages/framer-motion/cypress/integration/lazy-motion-fast-state.ts (uses
variants + a pre-load state change). The exact issue-2601 shape (plain
initial/animate objects, no state change) is not directly covered — Step 1
verifies it.
Item 2 is by design: initial is rendered into the style attribute
during render (server- and client-side) so the first paint never flashes the
animated end state; skipping it when features load late would introduce a
flash/hydration mismatch. That trade-off is inherent to code-splitting the
animation runtime and should be ruled on by the maintainer (expected ruling:
working as intended; users who need instant content should not put
opacity: 0 in initial, or should load features synchronously).
Create dev/react/src/tests/lazy-motion-initial.tsx (exporting App):
async LazyMotion (features={() => new Promise(r => setTimeout(() => r(domAnimation), 100))}) wrapping
<m.div id="box" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.1 }}>Hello</m.div>. Model on
dev/react/src/tests/lazy-motion-fast-state.tsx (set a
data-animation-complete attribute in onAnimationComplete).
Create packages/framer-motion/cypress/integration/lazy-motion-initial.ts
asserting: initially opacity is 0; after the features delay + animation,
#box has data-animation-complete and computed opacity 1.
Run on both React 18 and React 19 using the CLAUDE.md recipe (start Vite
directly in dev/react / dev/react-19 on a random port, cypress run --headed --spec cypress/integration/lazy-motion-initial.ts, React 19 with
--config-file=cypress.react-19.json).
Verify: spec passes on both React versions. If it fails → STOP and report:
the functional bug is NOT fully fixed and this issue needs a FIX plan rooted in
use-visual-element.ts / AnimationFeature.mount.
Branch issue-2601-verify, commit the two test files only. PR title:
"Add LazyMotion async-load initial animation test (closes #2601)". Note in the
body that the fix landed in v12.28.2 via #3501. (gh pr edit is broken on
this repo — if you need to edit, use gh api -X PATCH repos/motiondivision/motion/pulls/<n>.)
Open plans/issues/README.md, find the issue-2601 row. If not APPROVED, mark
this plan BLOCKED and stop.
gh api repos/motiondivision/motion/issues/2601/comments -f body="The functional part of this (animations not firing when LazyMotion features resolve after mount) was fixed in v12.28.2 — elements that mounted before features loaded now run their mount animation when features arrive, so content with initial={{opacity:0}} animate={{opacity:1}} becomes visible as soon as the bundle loads. Applying initial styles on first render is intentional: they must be present at first paint (and in SSR markup) to avoid a flash of the animated end state, which is the inherent trade-off of code-splitting the animation runtime. If first contentful paint matters more than the entrance animation, either load features synchronously or avoid hiding content via initial."
gh api -X PATCH repos/motiondivision/motion/issues/2601 -f state=closed -f state_reason=completed
Verify: state → "closed".
plans/issues/README.md status row updated