Repository: sveltejs/svelte (JavaScript)

Issue:
Local transitions playing on unmount since 5.38.3
### Describe the bug

When a component has a transition inside an #if block, the out transition will play when the component is unmounted, delaying the component unmount, even if specified as local

From  @7nik on bluesky:

"Seems to be caused by [github.com/sveltejs/sve...](https://github.com/sveltejs/svelte/pull/16625) and the fact that the if condition is in fact static."

### Reproduction

https://svelte.dev/playground/20daa9fc7f1740f89fb76099f14c51ae?version=latest

### Logs

```shell

```

### System Info

```shell
System:
    OS: macOS 15.6.1
    CPU: (8) arm64 Apple M1 Pro
    Memory: 103.92 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.18.0 - ~/.nvm/versions/node/v22.18.0/bin/node
    npm: 10.9.3 - ~/.nvm/versions/node/v22.18.0/bin/npm
    pnpm: 10.14.0 - ~/.nvm/versions/node/v22.18.0/bin/pnpm
  Browsers:
    Chrome: 140.0.7339.207
    Safari: 18.6
  npmPackages:
    svelte: ^5.39.5 => 5.39.5
```

### Severity

blocking an upgrade


Discussion:

Patch:
diff --git a/.changeset/easy-paths-take.md b/.changeset/easy-paths-take.md
new file mode 100644
index 000000000000..1378322abe5b
--- /dev/null
+++ b/.changeset/easy-paths-take.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: take into account static blocks when determining transition locality
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 1f35add2a88a..6818fd9d3039 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -14,7 +14,10 @@ export const DESTROYED = 1 << 14;
 
 // Flags exclusive to effects
 export const EFFECT_RAN = 1 << 15;
-/** 'Transparent' effects do not create a transition boundary */
+/**
+ * 'Transparent' effects do not create a transition boundary.
+ * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
+ */
 export const EFFECT_TRANSPARENT = 1 << 16;
 export const INSPECT_EFFECT = 1 << 17;
 export const HEAD_EFFECT = 1 << 18;
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index bfbb95a8db7c..9b54598f9e2b 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) {
 			(e.f & EFFECT_PRESERVED) === 0
 		) {
 			e = e.first;
+			if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
+				e.f |= EFFECT_TRANSPARENT;
+			}
 		}
 
 		if (e !== null) {
@@ -604,7 +607,12 @@ export function pause_children(effect, transitions, local) {
 
 	while (child !== null) {
 		var sibling = child.next;
-		var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
+		var transparent =
+			(child.f & EFFECT_TRANSPARENT) !== 0 ||
+			// If this is a branch effect without a block effect parent,
+			// it means the parent block effect was pruned. In that case,
+			// transparency information was transferred to the branch effect.
+			((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
 		// TODO we don't need to call pause_children recursively with a linked list in place
 		// it's slightly more involved though as we have to account for `transparent` changing
 		// through the tree.


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js
new file mode 100644
index 000000000000..900d6daff8a1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js
@@ -0,0 +1,22 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		const btn = target.querySelector('button');
+
+		btn?.click();
+		flushSync();
+		assert.htmlEqual(
+			target.innerHTML,
+			`
+			<button>Toggle</button>
+			<div>Should not transition out</div>
+		`
+		);
+
+		btn?.click();
+		flushSync();
+		assert.htmlEqual(target.innerHTML, '<button>Toggle</button>');
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte
new file mode 100644
index 000000000000..84f6ee77afe6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte
@@ -0,0 +1,18 @@
+
+<script>
+	import { slide } from 'svelte/transition';
+	let showText = $state(false);
+	let show = $state(true);
+</script>
+
+<button onclick={() => showText = !showText}>
+	Toggle
+</button>
+
+{#if showText}
+	{#if show}
+		<div transition:slide>
+			Should not transition out
+		</div>
+	{/if}
+{/if}