plans/issues/issue-2416.md
layout elementsExecutor 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 "STOP conditions" occurs, stop and report — do not improvise. When done, update (or add) this plan's row in
plans/issues/README.md.Drift check (run first):
gh api repos/motiondivision/motion/issues/2416 --jq .state→ expectopen. Re-read the "Current state" excerpts against the live files; on a mismatch, treat as a STOP condition.
42bfbe3ed, 2026-06-11The highest-traffic issue in this batch: mode="popLayout" + layout +
exit={{ opacity: 0 }} skips the exit fade on roughly every other removed
item — the element just vanishes. Reported 2023-11 (Sam Selikoff), with
"still happening" confirmations through 2025 (multiple users). Two strong
diagnostic clues from the thread: (1) replacing opacity with x: 100 in
exit makes it always work — the bug is opacity-specific; (2) one commenter
bisected the wider opacity-exit bug family (#2554/#2618/#2673) to the
v11.0.10 → v11.0.11 release. The repo has shipped many AnimatePresence fixes
since (12.36.x–12.40.0), so the FIRST job is an honest reproduction on
current main; only then fix.
github/samselikoff/2023-11-25-framer-motion-pop-layout-bug was
Cloudflare-blocked from the planning environment; retry once via
WebFetch, but the inline steps are sufficient):
<AnimatePresence mode="popLayout">; each item is a
motion.div with layout, exit={{ opacity: 0 }}, unique key.git log --oneline v11.0.10..v11.0.11 — the
substantive change is f949a899c "Fix/async animation 2 (#2528)" (async
animation start / WAAPI changes). If the bug still reproduces, diff that
area first.git show <sha>
before assuming the bug persists):
90d8c5364 "Fix AnimatePresence not removing children when exit matches current values" (12.39.0 era)aa8b46be3 "Fix duplicate exit animation processing in AnimatePresence"8798d7017 "Fix AnimatePresence keeping exiting children in DOM during rapid updates…"0d38f5623 "Remove data-motion-pop-id attribute when popLayout exit is interrupted"packages/framer-motion/src/components/AnimatePresence/index.tsx —
diffing + onExit (lines 188–213), exitComplete/exitingComponents
bookkeeping (lines 88–121).packages/framer-motion/src/components/AnimatePresence/PopChild.tsx —
pop measurement (getSnapshotBeforeUpdate, lines 38–62) and the injected
position: absolute style (lines 105–141).plans/issues/pr-3707.md) fixes a different stuck-exit
(#3243, child unmounts mid-exit). Don't duplicate it.packages/framer-motion/cypress/integration/animate-presence-pop.ts,
test pages dev/react/src/tests/animate-presence-pop-list.tsx,
animate-presence-pop-interrupt.tsx.| Purpose | Command | Expected |
|---|---|---|
| Build | yarn build (repo root) | exit 0 |
| Cypress (primary) | CLAUDE.md § "Running Cypress tests locally" — React 18 AND React 19 | spec fails pre-fix, passes post-fix |
| Jest sweep | npx jest --config packages/framer-motion/jest.config.json --testPathPattern="AnimatePresence" | pass |
| Issue state | gh api repos/motiondivision/motion/issues/2416 --jq .state | open |
| Gated close (only if not reproducible AND row approved) | gh api -X PATCH repos/motiondivision/motion/issues/2416 -f state=closed -f state_reason=not_planned | state closed |
In scope:
dev/react/src/tests/animate-presence-pop-layout-exit-opacity.tsx (create)packages/framer-motion/cypress/integration/animate-presence-pop-layout-exit-opacity.ts (create)packages/framer-motion/src/components/AnimatePresence/ and/or
packages/motion-dom/src/animation/ (WAAPI opacity path)CHANGELOG.mdOut of scope: iframe/rAF-throttling variant (kitsunekyo's comment); projection-system refactors; PR #3707's diff; issues #2554/#2618/#2673 (closed — reference only).
Branch fix/issue-2416-poplayout-exit-opacity. Short imperative commits.
gh pr edit is broken — use gh api -X PATCH repos/motiondivision/motion/pulls/<n>.
Test page animate-presence-pop-layout-exit-opacity.tsx: a column of 4 items,
each motion.div with layout, exit={{ opacity: 0 }},
transition={{ type: "tween", ease: "linear", duration: 10 }} (long + linear,
per CLAUDE.md mid-animation guidance), removed on click, ids item-0..3,
inside <AnimatePresence mode="popLayout">.
Spec: remove item-1; after ~500ms use .then() (NOT .should()) to assert
getComputedStyle(el).opacity is strictly between 0.9 and 1 and the element
is still in the DOM; wait for unmount; remove item-2; repeat the mid-animation
check. The reported bug makes the second removal vanish instantly — the
mid-animation check on removal #2 is the regression gate. Also assert via
el.getAnimations() that an opacity animation exists (opacity IS a compositor
property — allowed per CLAUDE.md).
Verify: run on React 18 per the CLAUDE.md recipe. Record the outcome of
the FIRST run (tail -60).
animate={{ opacity: 1 }},
try removal immediately after a previous exit completes). Max 3
variations. If it still passes everywhere → go to Step 6 (verified-fixed
path). Do NOT keep tuning beyond 3 attempts.In order:
onAnimationStart/onAnimationComplete on the item.x works and opacity doesn't,
suspect WAAPI/willChange/value-reset interaction. Inspect
90d8c5364's "exit matches current values" logic for a false positive on
the second item (e.g. opacity read as already 0 from the previous item's
torn-down style, or a stale MotionValue shared via recycled
VisualElement state).exitComplete/exitingComponents bookkeeping in index.tsx for the
alternating pattern — alternation smells like a flag toggled by the
previous exit's completion (e.g. exitingComponents Set entry not cleared,
line 118, or exitComplete map state leaking between consecutive exits).f949a899c (v11.0.11 async animation change) for the
historical mechanism.Smallest change that makes the Step 1 spec pass without breaking the
AnimatePresence Jest suite or the other animate-presence-* Cypress specs.
Full AnimatePresence Jest pattern; Cypress animate-presence-pop*.ts plus the
new spec on React 18 AND 19; CHANGELOG entry. Then open the PR referencing
#2416 (and note whether #2554/#2618/#2673 reporters should re-test).
Keep the new page+spec as regression coverage ONLY if the maintainer wants it
(per repo policy, no speculative happy-path coverage — default is to DISCARD
the fixture and not open a PR). Comment on #2416: state the exact commit
tested (42bfbe3ed / 12.40.0), the spec you ran, both React versions, and ask
reporters to confirm on ≥12.40.0; mention the iframe-rAF sub-thread is a
separate mechanism. Closing is gated: if this plan's row in
plans/issues/README.md is not marked APPROVED (or APPROVED-CLOSE), set the
row to BLOCKED("awaiting maintainer close approval") and stop.
cypress/integration/animate-presence-pop.ts (structure),
CLAUDE.md "Cypress animation testing patterns" (mid-animation .then()
measurements, long linear tween).npx jest --config packages/framer-motion/jest.config.json --testPathPattern="AnimatePresence" exits 0animate-presence-pop*.ts specs still green on both React versionsyarn build exits 0; CHANGELOG updatedplans/issues/README.md row updated42bfbe3ed, both React versionsprojection/) — stop and
report with findings; that's a bigger change than this plan authorizes.PresenceChild register/cleanup where PR #3707
operates — stop; #3707 lands first.plans/issues/README.md cross-cutting facts) — re-run once, then stop.issue-2684.md). If a real fix lands,
comment on those closed threads.