From 656a77142bf2ead7e96dd63b14ccca17b2801709 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 6 Feb 2026 10:48:50 +0100 Subject: [PATCH 1/2] Fix stale shared layout nodes during SPA navigations In SPA frameworks, page navigation removes DOM elements externally without going through Motion's projection unmount path. This left zombie projection nodes in the NodeStack, causing broken layout animations when new elements with the same layoutId mounted. Guard against stale nodes in three places: - promote(): skip setting resumeFrom when prevLead has a disconnected instance and no snapshot (zombie from external removal) - relegate(): skip disconnected candidates when finding a new lead - add(): prune zombie members (but preserve lead/prevLead for the current animation cycle) Co-Authored-By: Claude Opus 4.6 --- .../shared-element-spa-navigation.html | 102 ++++++++++++++++++ .../fixtures/animate-layout-tests.json | 2 +- .../motion-dom/src/projection/shared/stack.ts | 40 ++++--- 3 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 dev/html/public/animate-layout/shared-element-spa-navigation.html diff --git a/dev/html/public/animate-layout/shared-element-spa-navigation.html b/dev/html/public/animate-layout/shared-element-spa-navigation.html new file mode 100644 index 0000000000..48a2d69bf2 --- /dev/null +++ b/dev/html/public/animate-layout/shared-element-spa-navigation.html @@ -0,0 +1,102 @@ + + + + + +
+
+
+ + + + + + diff --git a/packages/framer-motion/cypress/fixtures/animate-layout-tests.json b/packages/framer-motion/cypress/fixtures/animate-layout-tests.json index 35a130ed29..380c7d5516 100644 --- a/packages/framer-motion/cypress/fixtures/animate-layout-tests.json +++ b/packages/framer-motion/cypress/fixtures/animate-layout-tests.json @@ -1 +1 @@ -["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"] \ No newline at end of file +["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-element-spa-navigation.html","shared-multiple-elements.html"] \ No newline at end of file diff --git a/packages/motion-dom/src/projection/shared/stack.ts b/packages/motion-dom/src/projection/shared/stack.ts index b391c990fc..6f2a85dd40 100644 --- a/packages/motion-dom/src/projection/shared/stack.ts +++ b/packages/motion-dom/src/projection/shared/stack.ts @@ -8,6 +8,16 @@ export class NodeStack { add(node: IProjectionNode) { addUniqueItem(this.members, node) + + for (let i = this.members.length - 1; i >= 0; i--) { + const m = this.members[i] + if (m === node || m === this.lead || m === this.prevLead) continue + const inst = m.instance as HTMLElement | undefined + if (inst && inst.isConnected === false && m.isPresent !== false && !m.snapshot) { + removeItem(this.members, m) + } + } + node.scheduleRender() } @@ -34,7 +44,8 @@ export class NodeStack { let prevLead: IProjectionNode | undefined for (let i = indexOfNode; i >= 0; i--) { const member = this.members[i] - if (member.isPresent !== false) { + const inst = member.instance as HTMLElement | undefined + if (member.isPresent !== false && (!inst || inst.isConnected !== false)) { prevLead = member break } @@ -76,20 +87,25 @@ export class NodeStack { prevDep === nextDep if (!dependencyMatches) { - node.resumeFrom = prevLead + const prevInstance = prevLead.instance as HTMLElement | undefined + const isStale = prevInstance && prevInstance.isConnected === false && !prevLead.snapshot - if (preserveFollowOpacity) { - node.resumeFrom.preserveOpacity = true - } + if (!isStale) { + node.resumeFrom = prevLead - if (prevLead.snapshot) { - node.snapshot = prevLead.snapshot - node.snapshot.latestValues = - prevLead.animationValues || prevLead.latestValues - } + if (preserveFollowOpacity) { + node.resumeFrom.preserveOpacity = true + } + + if (prevLead.snapshot) { + node.snapshot = prevLead.snapshot + node.snapshot.latestValues = + prevLead.animationValues || prevLead.latestValues + } - if (node.root && node.root.isUpdating) { - node.isLayoutDirty = true + if (node.root && node.root.isUpdating) { + node.isLayoutDirty = true + } } } From 01e43080457a6e5f4e218ed8a70072006f4cdb48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:50:04 +0000 Subject: [PATCH 2/2] chore(deps): bump next from 15.4.10 to 15.5.10 Bumps [next](https://github.com/vercel/next.js) from 15.4.10 to 15.5.10. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.4.10...v15.5.10) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.10 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- dev/next/package.json | 2 +- yarn.lock | 86 +++++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/dev/next/package.json b/dev/next/package.json index 61da8f8a81..f03646c9a0 100644 --- a/dev/next/package.json +++ b/dev/next/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "motion": "^12.33.1", - "next": "15.4.10", + "next": "15.5.10", "react": "19.0.0", "react-dom": "19.0.0" } diff --git a/yarn.lock b/yarn.lock index a86c263107..e8bc0312c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2135,65 +2135,65 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.4.10": - version: 15.4.10 - resolution: "@next/env@npm:15.4.10" - checksum: b89dd1279a53c69d86630b63cb550cbacaed28b8e423f73c492581a17cb4c1bda4429ab3c19b3ec24c2a65f06e55d954b553c6553ee94eb2867c628a3d291e04 +"@next/env@npm:15.5.10": + version: 15.5.10 + resolution: "@next/env@npm:15.5.10" + checksum: cc481d2bd5a7cf97a0d3c093bb9a9b5359ad324c9eb26c89621c90f8d8d11a284fd05cdcc5358db973074fb65c0d9aa01ecbc464b181c9fe3b2bca0776a0fc54 languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-darwin-arm64@npm:15.4.8" +"@next/swc-darwin-arm64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-arm64@npm:15.5.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-darwin-x64@npm:15.4.8" +"@next/swc-darwin-x64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-x64@npm:15.5.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-linux-arm64-gnu@npm:15.4.8" +"@next/swc-linux-arm64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-linux-arm64-musl@npm:15.4.8" +"@next/swc-linux-arm64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-linux-x64-gnu@npm:15.4.8" +"@next/swc-linux-x64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-linux-x64-musl@npm:15.4.8" +"@next/swc-linux-x64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-musl@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-win32-arm64-msvc@npm:15.4.8" +"@next/swc-win32-arm64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.5.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.4.8": - version: 15.4.8 - resolution: "@next/swc-win32-x64-msvc@npm:15.4.8" +"@next/swc-win32-x64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.5.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -11154,25 +11154,25 @@ __metadata: resolution: "next-env@workspace:dev/next" dependencies: motion: ^12.33.1 - next: 15.4.10 + next: 15.5.10 react: 19.0.0 react-dom: 19.0.0 languageName: unknown linkType: soft -"next@npm:15.4.10": - version: 15.4.10 - resolution: "next@npm:15.4.10" - dependencies: - "@next/env": 15.4.10 - "@next/swc-darwin-arm64": 15.4.8 - "@next/swc-darwin-x64": 15.4.8 - "@next/swc-linux-arm64-gnu": 15.4.8 - "@next/swc-linux-arm64-musl": 15.4.8 - "@next/swc-linux-x64-gnu": 15.4.8 - "@next/swc-linux-x64-musl": 15.4.8 - "@next/swc-win32-arm64-msvc": 15.4.8 - "@next/swc-win32-x64-msvc": 15.4.8 +"next@npm:15.5.10": + version: 15.5.10 + resolution: "next@npm:15.5.10" + dependencies: + "@next/env": 15.5.10 + "@next/swc-darwin-arm64": 15.5.7 + "@next/swc-darwin-x64": 15.5.7 + "@next/swc-linux-arm64-gnu": 15.5.7 + "@next/swc-linux-arm64-musl": 15.5.7 + "@next/swc-linux-x64-gnu": 15.5.7 + "@next/swc-linux-x64-musl": 15.5.7 + "@next/swc-win32-arm64-msvc": 15.5.7 + "@next/swc-win32-x64-msvc": 15.5.7 "@swc/helpers": 0.5.15 caniuse-lite: ^1.0.30001579 postcss: 8.4.31 @@ -11215,7 +11215,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 11f76f5de9f0e734dd3925a48821e29291d82191a099d2e35695d51acbcbd4ee965d2842748e4a95cfdf55f38e3edb2180fd3be4adbe36b01936a7130ad60a11 + checksum: c60fc5eb6d2fc7868a110a713fc772e69dfb0b13a64dc6ddb3b79a37bfe88c870433959337693b33bc88c1086a0c42e25b6eafbb6e198c935e7e7bff213c40e9 languageName: node linkType: hard