From 5d1fa3cf493286d25579f1bf745b4f26cdadcdb8 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 26 May 2026 19:04:37 -0700 Subject: [PATCH 1/2] fix(astro): Preserve clerk component styling across view transitions --- .../astro-view-transition-styling-fix.md | 9 +++++ .../src/pages/transitions/index.astro | 1 + .../src/pages/transitions/page2.astro | 11 ++++++ integration/tests/astro/components.test.ts | 36 +++++++++++++++++++ packages/astro/src/integration/snippets.ts | 34 ++++++++++++------ 5 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 .changeset/astro-view-transition-styling-fix.md create mode 100644 integration/templates/astro-node/src/pages/transitions/page2.astro diff --git a/.changeset/astro-view-transition-styling-fix.md b/.changeset/astro-view-transition-styling-fix.md new file mode 100644 index 00000000000..41e8c5998f0 --- /dev/null +++ b/.changeset/astro-view-transition-styling-fix.md @@ -0,0 +1,9 @@ +--- +'@clerk/astro': patch +--- + +Fix Clerk component styling being lost after Astro View Transitions navigations. + +The `#clerk-components` element (which hosts Clerk's React root) was being cloned rather than moved during the `astro:before-swap` phase. The clone carried no React association, so the React root ended up bound to a detached element after the body swap, breaking style injection on subsequent navigations. + +Also consolidates the two `import('astro:transitions/client')` calls inside the event listeners into a single eagerly-started Promise shared by both handlers. diff --git a/integration/templates/astro-node/src/pages/transitions/index.astro b/integration/templates/astro-node/src/pages/transitions/index.astro index 3308cd1d7a1..2dc2cc41dc3 100644 --- a/integration/templates/astro-node/src/pages/transitions/index.astro +++ b/integration/templates/astro-node/src/pages/transitions/index.astro @@ -10,6 +10,7 @@ import Layout from '../../layouts/ViewTransitionsLayout.astro'; + Page 2 diff --git a/integration/templates/astro-node/src/pages/transitions/page2.astro b/integration/templates/astro-node/src/pages/transitions/page2.astro new file mode 100644 index 00000000000..4fe2e2169e6 --- /dev/null +++ b/integration/templates/astro-node/src/pages/transitions/page2.astro @@ -0,0 +1,11 @@ +--- +import { UserButton } from '@clerk/astro/components'; +import Layout from '../../layouts/ViewTransitionsLayout.astro'; +--- + + +
+ Back + +
+
diff --git a/integration/tests/astro/components.test.ts b/integration/tests/astro/components.test.ts index 4919fa96ec8..f015a9a7f84 100644 --- a/integration/tests/astro/components.test.ts +++ b/integration/tests/astro/components.test.ts @@ -484,6 +484,42 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f await u.po.userButton.waitForMounted(); }); + test('preserves clerk component styling after view transitions navigations', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + // Sign in directly (full navigation, not view transition) + await u.page.goToRelative('/sign-in'); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + // Go to the view-transitions-enabled page + await u.page.goToRelative('/transitions'); + await u.po.userButton.waitForMounted(); + + // Navigate to a second transitions page via link click (triggers view transition) + await u.page.getByRole('link', { name: /Page 2/i }).click(); + await u.page.waitForURL(`${app.serverUrl}/transitions/page2`); + await u.po.userButton.waitForMounted(); + + // Navigate back via link click (triggers another view transition) + await u.page.getByRole('link', { name: /Back/i }).click(); + await u.page.waitForURL(`${app.serverUrl}/transitions`); + await u.po.userButton.waitForMounted(); + + // Verify Emotion style elements are present in document.head after the round-trip. + // Regression: cloneNode() on #clerk-components detached the React root from its host + // element, causing Clerk to lose its style injection context on subsequent navigations. + const emotionStyleCount = await page.evaluate(() => document.head.querySelectorAll('style[data-emotion]').length); + expect(emotionStyleCount).toBeGreaterThan(0); + + // Verify #clerk-components is attached to the live document (not detached from the React root). + const clerkRootIsAttached = await page.evaluate(() => + document.body.contains(document.getElementById('clerk-components')), + ); + expect(clerkRootIsAttached).toBe(true); + }); + test('server islands Show component shows correct states', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); diff --git a/packages/astro/src/integration/snippets.ts b/packages/astro/src/integration/snippets.ts index 698bf70c87f..7b04bf4bba8 100644 --- a/packages/astro/src/integration/snippets.ts +++ b/packages/astro/src/integration/snippets.ts @@ -71,29 +71,41 @@ export function buildPageLoadSnippet({ } if (transitionEnabledOnThisPage()) { - // We must do the dynamic imports within the event listeners because otherwise we may race and miss initial astro:page-load - document.addEventListener('astro:before-swap', async (e) => { - const { swapFunctions } = await import('astro:transitions/client'); + // Start loading eagerly without awaiting so both listeners share one module load. + // Listeners are registered synchronously here, which avoids the race where awaiting + // before addEventListener would cause us to miss the initial astro:page-load event. + const transitionClient = import('astro:transitions/client'); - const clerkComponents = document.querySelector('#clerk-components'); - // Keep the div element added by Clerk - if (clerkComponents) { - const clonedEl = clerkComponents.cloneNode(true); - e.newDocument.body.appendChild(clonedEl); + document.addEventListener('astro:before-swap', async (e) => { + const nextDocument = e.newDocument; + const nextHead = nextDocument?.head; + if (!nextDocument || !nextHead) { + return; } - e.swap = () => swapDocument(swapFunctions, e.newDocument); + const { swapFunctions } = await transitionClient; + + e.swap = () => { + const clerkComponents = document.querySelector('#clerk-components'); + // Move (not clone) the element so Clerk's React root stays bound to its host node + // across the body swap. Cloning produces a detached copy with no React associated, + // which breaks style injection on subsequent navigations. + if (clerkComponents) { + nextDocument.body.appendChild(clerkComponents); + } + swapDocument(swapFunctions, nextDocument); + }; }); document.addEventListener('astro:page-load', async (e) => { - const { navigate } = await import('astro:transitions/client'); + const { navigate } = await transitionClient; await runInjectionScript({ ...${JSON.stringify(internalParams)}, routerPush: navigate, routerReplace: (url) => navigate(url, { history: 'replace' }), }); - }) + }); } else { await runInjectionScript(${JSON.stringify(internalParams)}); }`; From ceffc1e6204b41676fc710ea11a5fb6dde39df27 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Tue, 26 May 2026 19:49:05 -0700 Subject: [PATCH 2/2] chore: update changeset --- .changeset/astro-view-transition-styling-fix.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.changeset/astro-view-transition-styling-fix.md b/.changeset/astro-view-transition-styling-fix.md index 41e8c5998f0..071f8b04caa 100644 --- a/.changeset/astro-view-transition-styling-fix.md +++ b/.changeset/astro-view-transition-styling-fix.md @@ -3,7 +3,3 @@ --- Fix Clerk component styling being lost after Astro View Transitions navigations. - -The `#clerk-components` element (which hosts Clerk's React root) was being cloned rather than moved during the `astro:before-swap` phase. The clone carried no React association, so the React root ended up bound to a detached element after the body swap, breaking style injection on subsequent navigations. - -Also consolidates the two `import('astro:transitions/client')` calls inside the event listeners into a single eagerly-started Promise shared by both handlers.