plans/issues/issue-2338.md
Executor instructions: Verification-first plan; repo policy is no repro → no fix. The sandbox is unreachable, but the issue states exactly how it was built ("the docs' Shared layout animations example plus a hide/show toggle"), so reconstruction fidelity is high. Honor the approval gate before closing. Update the row in
plans/issues/README.mdwhen done.Drift check (run first):
gh api repos/motiondivision/motion/issues/2338 --jq .state→ must beopen.
42bfbe3ed, 2026-06-11Sept 2023 report: a LayoutGroup tabs row (docs "Shared layout animations"
example — tab underline with layoutId="underline") is hidden then shown;
on becoming visible the underline animates in from a seemingly random
location instead of preserving its state. Reproduced by the reporter on
Chrome/Safari/Firefox/Arc at the time. Two later fixes target precisely this
state-staleness class, so the issue may be dead:
90a3dfbda "Discard zero snapshots (#3030)" (2025-01): updateSnapshot()
now discards measurements with zero width AND height
(packages/motion-dom/src/projection/node/create-projection-node.ts:885-897)
— kills the "animate from a 0×0 box at the viewport origin" failure mode
that a hidden/detached element produces.656a77142 + ea1448e4b (2026-02): NodeStack.add() now evicts
disconnected members without snapshots
(packages/motion-dom/src/projection/shared/stack.ts:12-20) — kills stale
leads surviving an unmount/remount cycle.But neither covers every hiding mechanism, and we don't know which one the sandbox used — that's the investigation.
codesandbox.io/s/framer-motion-layout-animations-visibility-bug-37wvkh
unreachable at planning time (Cloudflare 403). Retry once before
reconstructing.NodeStack eviction +
promote() snapshot adoption (stack.ts:45-73);display: none toggle — element stays mounted; when hidden, any
measurement is a zero box (now discarded per #3030), but a didUpdate
while hidden may still mark layout dirty;visibility: hidden — element keeps its box; measurements stay valid;hidden attribute — same as display:none.updateSnapshot() (lines 885-897),
unmount() → this.options.layoutId && this.willUpdate() (line 609),
scheduleCheckAfterUnmount() (lines 831-844), and NodeStack.promote()
copying prevLead.snapshot (stack.ts:64-68).Standard Cypress recipe from CLAUDE.md (Vite dev/react random port → spec;
then dev/react-19 + cypress.react-19.json). Spec name:
cypress/integration/layout-group-visibility-toggle.ts.
In scope:
dev/react/src/tests/layout-group-visibility-toggle.tsx (create)packages/framer-motion/cypress/integration/layout-group-visibility-toggle.ts (create)updateSnapshot/notifyLayoutUpdate
(create-projection-node.ts) or NodeStack eviction. Report before
changes >30 lines (PRs #3748/#3749 own this file's future).Out of scope:
Test page layout-group-visibility-toggle.tsx: the classic tabs strip —
3 tabs, each <motion.li> containing, when selected,
<motion.div layoutId="underline" id="underline" />, wrapped in
<LayoutGroup>. Add #toggle which hides/shows the whole strip. Make the
hiding mechanism a prop cycled by the test page so one page covers all four
modes (?mode=unmount|display|visibility|hidden via query param — the dev
app passes search params through). Underline transition:
{ type: "tween", ease: "linear", duration: 0.3 }, and record
onLayoutAnimationStart into a data-anim-count attribute on the underline
so the spec can assert "NO animation happened".
For each mode: select tab 2 → wait for settle → capture underline rect →
toggle hide → toggle show → after cy.wait(100), assert via .then():
data-anim-count did not increase across the hide/show cycle (the issue's
expected behavior: "There should be no animation on the element becoming
visible again").Verify: run all modes on React 18. Record a verdict per mode.
Instrument via cy.window(): dump projection.snapshot and stack state on
show. Expected culprits by mode: zero-area box that survives the #3030 guard
because only ONE axis is zero (updateSnapshot discards only when BOTH
axes have zero length — !calcLength(x) && !calcLength(y), line 890-896 —
a display:none child of a sized parent can measure 0×N), or a stale
snapshot adopted in promote(). Fix narrowly; failing test goes green; HTML
projection suite + layout-group.ts (flaky — re-run once) stay green; run
React 18 AND 19.
Comment on #2338: reconstruction at <commit> across four hiding modes, all
clean; likely fixed by 90a3dfbda/656a77142; sandbox unreachable — ask
reporter to re-test on ≥12.34. Recommend closing. Close ONLY after the
plans/issues/README.md row reads APPROVED-CLOSE:
gh api -X PATCH repos/motiondivision/motion/issues/2338 -f state=closed -f state_reason=not_planned.
Do not land the never-failing test (policy); attach the test page code to the
comment instead.
APPROVED-CLOSEplans/issues/README.md row updatedcypress run --browser chrome) — environment artifact; report instead of fixing (memory note:
don't overstate Electron-only limits, verify in Chrome).promote() snapshot adoption semantics — that path
is shared with issues #2405/#1411; report so changes are coordinated.updateSnapshot (line 890: requires BOTH axes
zero) is worth flagging to the maintainer even if this issue doesn't
reproduce — cheap hardening candidate, but only with a test that proves it.