diff --git a/.changeset/fix-safari-persist-canvas-context-loss.md b/.changeset/fix-safari-persist-canvas-context-loss.md
new file mode 100644
index 000000000000..3f99fec94b95
--- /dev/null
+++ b/.changeset/fix-safari-persist-canvas-context-loss.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improves internal state retention for persisted elements during view transitions, especially avoiding WebGL context loss in Safari and resets of CSS transitions and iframes in modern Chromium and Firefox browsers
diff --git a/.changeset/wet-animals-pump.md b/.changeset/wet-animals-pump.md
new file mode 100644
index 000000000000..780c54661a05
--- /dev/null
+++ b/.changeset/wet-animals-pump.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/internal-helpers': minor
+---
+
+Adds the new utilities `MANY_LEADING_SLASHES` and `collapseDuplicateLeadingSlashes`.
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-one.astro
new file mode 100644
index 000000000000..d2789f4c817e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-one.astro
@@ -0,0 +1,15 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+ Canvas Page 1
+ go to 2
+ go to page without canvas
+
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-two.astro
new file mode 100644
index 000000000000..1f79c65b4118
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/canvas-persist-two.astro
@@ -0,0 +1,8 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+ Canvas Page 2
+ go to 1
+
+
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 4085d3ffd6c9..9b135e9325f1 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -1578,6 +1578,52 @@ test.describe('View Transitions', () => {
expect(text).toBe('true true');
});
+ test('transition:persist preserves canvas pixel data across navigation', async ({
+ page,
+ astro,
+ }) => {
+ // Page 1 draws a red rectangle on a persisted canvas
+ await page.goto(astro.resolveUrl('/canvas-persist-one'));
+ await expect(page.locator('#canvas-one')).toHaveText('Canvas Page 1');
+
+ // Verify the red rectangle was drawn (pixel at 20,20 should be red)
+ const beforePixel = await page.$eval('#my-canvas', (c) => {
+ const ctx = c.getContext('2d');
+ const pixel = ctx.getImageData(20, 20, 1, 1).data;
+ return [pixel[0], pixel[1], pixel[2], pixel[3]];
+ });
+ expect(beforePixel).toEqual([255, 0, 0, 255]);
+
+ // Navigate via client-side link (View Transitions swap runs)
+ await page.click('#click-two');
+ await expect(page.locator('#canvas-two')).toHaveText('Canvas Page 2');
+
+ // The persisted canvas should retain its pixel data
+ const afterPixel = await page.$eval('#my-canvas', (c) => {
+ const ctx = c.getContext('2d');
+ const pixel = ctx.getImageData(20, 20, 1, 1).data;
+ return [pixel[0], pixel[1], pixel[2], pixel[3]];
+ });
+ expect(afterPixel).toEqual([255, 0, 0, 255]);
+ });
+
+ test('transition:persist drops elements without matching target in new page', async ({
+ page,
+ astro,
+ }) => {
+ // Canvas page has a canvas with transition:persist="my-canvas"
+ await page.goto(astro.resolveUrl('/canvas-persist-one'));
+ await expect(page.locator('#canvas-one')).toHaveText('Canvas Page 1');
+ expect(await page.locator('#my-canvas').count()).toBe(1);
+
+ // Navigate via client-side link to a page WITHOUT a matching persist target
+ await page.click('#click-no-canvas');
+ await expect(page.locator('#one')).toHaveText('Page 1');
+
+ // The persisted canvas should NOT appear on the new page
+ expect(await page.locator('#my-canvas').count()).toBe(0);
+ });
+
test('it should be easy to define a data-theme preserving swap function', async ({
page,
astro,
diff --git a/packages/astro/src/transitions/swap-functions.ts b/packages/astro/src/transitions/swap-functions.ts
index 9fb7e505118f..5b73ce5e7084 100644
--- a/packages/astro/src/transitions/swap-functions.ts
+++ b/packages/astro/src/transitions/swap-functions.ts
@@ -71,25 +71,50 @@ export function swapHeadElements(doc: Document) {
}
export function swapBodyElement(newElement: Element, oldElement: Element) {
- // this will reset scroll Position
- oldElement.replaceWith(newElement);
+ // Lift persist elements to before the body swap so they stay in the DOM
+ // throughout replaceWith(). This prevents Safari from losing WebGL context on
+ //