diff --git a/.changeset/late-pigs-study.md b/.changeset/late-pigs-study.md
new file mode 100644
index 00000000000..0b521cd1d32
--- /dev/null
+++ b/.changeset/late-pigs-study.md
@@ -0,0 +1,5 @@
+---
+"@clerk/astro": minor
+---
+
+Add support for Astro View Transitions
diff --git a/integration/templates/astro-node/src/layouts/ViewTransitionsLayout.astro b/integration/templates/astro-node/src/layouts/ViewTransitionsLayout.astro
new file mode 100644
index 00000000000..a01199898b7
--- /dev/null
+++ b/integration/templates/astro-node/src/layouts/ViewTransitionsLayout.astro
@@ -0,0 +1,27 @@
+---
+import { ViewTransitions } from 'astro:transitions';
+
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/transitions/index.astro b/integration/templates/astro-node/src/pages/transitions/index.astro
new file mode 100644
index 00000000000..a9465f5eb46
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/transitions/index.astro
@@ -0,0 +1,15 @@
+---
+import { SignedIn, SignedOut, UserButton } from "@clerk/astro/components";
+import Layout from "../../layouts/ViewTransitionsLayout.astro";
+---
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/transitions/sign-in.astro b/integration/templates/astro-node/src/pages/transitions/sign-in.astro
new file mode 100644
index 00000000000..5afdd5a3489
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/transitions/sign-in.astro
@@ -0,0 +1,10 @@
+---
+import { SignIn } from "@clerk/astro/components";
+import Layout from "../../layouts/ViewTransitionsLayout.astro";
+---
+
+
+
+
+
+
diff --git a/integration/tests/astro/components.test.ts b/integration/tests/astro/components.test.ts
index 8f9b3f9dfd3..ed88f2a29e4 100644
--- a/integration/tests/astro/components.test.ts
+++ b/integration/tests/astro/components.test.ts
@@ -451,4 +451,34 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await u.po.expect.toBeSignedIn();
await expect(u.page.getByText('Not a member')).toBeVisible();
});
+
+ test('renders components and keep internal routing behavior when view transitions is enabled', async ({
+ page,
+ context,
+ }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/transitions');
+ // Navigate to sign-in page using link to simulate transition
+ await u.page.getByRole('link', { name: /Sign in/i }).click();
+
+ // Components should be mounted on the new document
+ // when navigating through links
+ await u.page.waitForURL(`${app.serverUrl}/transitions/sign-in`);
+ await u.po.signIn.waitForMounted();
+
+ await u.po.signIn.setIdentifier(fakeAdmin.email);
+ await u.po.signIn.continue();
+ await u.page.waitForURL(`${app.serverUrl}/transitions/sign-in#/factor-one`);
+
+ await u.po.signIn.setPassword(fakeAdmin.password);
+ await u.po.signIn.continue();
+
+ await u.po.expect.toBeSignedIn();
+
+ // Internal Clerk routing should still work
+ await u.page.waitForURL(`${app.serverUrl}/transitions`);
+
+ // Components should be rendered on hard reload
+ await u.po.userButton.waitForMounted();
+ });
});
diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts
index b640dedbbf4..4505b0d939d 100644
--- a/packages/astro/src/integration/create-integration.ts
+++ b/packages/astro/src/integration/create-integration.ts
@@ -109,8 +109,31 @@ function createIntegration()
'page',
`
${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: page")` : ''}
- import { runInjectionScript } from "${buildImportPath}";
- await runInjectionScript(${JSON.stringify(internalParams)});`,
+ import { runInjectionScript, swapDocument } from "${buildImportPath}";
+ import { navigate, transitionEnabledOnThisPage } from "astro:transitions/client";
+
+ if (transitionEnabledOnThisPage()) {
+ document.addEventListener('astro:before-swap', (e) => {
+ 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);
+ }
+
+ e.swap = () => swapDocument(e.newDocument);
+ });
+
+ document.addEventListener('astro:page-load', async (e) => {
+ await runInjectionScript({
+ ...${JSON.stringify(internalParams)},
+ routerPush: navigate,
+ routerReplace: (url) => navigate(url, { history: 'replace' }),
+ });
+ })
+ } else {
+ await runInjectionScript(${JSON.stringify(internalParams)});
+ }`,
);
},
},
diff --git a/packages/astro/src/integration/vite-plugin-astro-config.ts b/packages/astro/src/integration/vite-plugin-astro-config.ts
index dfc60db1209..f7f511a4d4c 100644
--- a/packages/astro/src/integration/vite-plugin-astro-config.ts
+++ b/packages/astro/src/integration/vite-plugin-astro-config.ts
@@ -26,6 +26,8 @@ export function vitePluginAstroConfig(astroConfig: AstroConfig): VitePlugin {
// This ensures @clerk/astro/client is properly processed and bundled,
// resolving runtime import issues in these components.
config.optimizeDeps?.include?.push('@clerk/astro/client');
+ // Let astro vite plugin handle this.
+ config.optimizeDeps?.exclude?.push('astro:transitions/client');
},
load(id) {
if (id === resolvedVirtualModuleId) {
diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts
index da6e4b11dcd..597519dda58 100644
--- a/packages/astro/src/internal/create-clerk-instance.ts
+++ b/packages/astro/src/internal/create-clerk-instance.ts
@@ -45,9 +45,9 @@ async function createClerkInstanceInternal(options?: AstroClerkCreateInstancePar
}
initOptions = {
- ...options,
routerPush: createNavigationHandler(window.history.pushState.bind(window.history)),
routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)),
+ ...options,
};
return clerkJSInstance
diff --git a/packages/astro/src/internal/index.ts b/packages/astro/src/internal/index.ts
index c00256cf3f8..24c1b6670a9 100644
--- a/packages/astro/src/internal/index.ts
+++ b/packages/astro/src/internal/index.ts
@@ -13,3 +13,4 @@ const runInjectionScript = createInjectionScriptRunner(createClerkInstance);
export { runInjectionScript };
export { generateSafeId } from './utils/generateSafeId';
+export { swapDocument } from './swap-document';
diff --git a/packages/astro/src/internal/swap-document.ts b/packages/astro/src/internal/swap-document.ts
new file mode 100644
index 00000000000..0bf08713baa
--- /dev/null
+++ b/packages/astro/src/internal/swap-document.ts
@@ -0,0 +1,61 @@
+// eslint-disable-next-line import/no-unresolved
+import { swapFunctions } from 'astro:transitions/client';
+
+const PERSIST_ATTR = 'data-astro-transition-persist';
+const EMOTION_ATTR = 'data-emotion';
+
+/**
+ * @internal
+ * Custom swap function to make mounting and styling
+ * of Clerk components work with View Transitions in Astro.
+ *
+ * See https://docs.astro.build/en/guides/view-transitions/#building-a-custom-swap-function
+ */
+export function swapDocument(doc: Document) {
+ swapFunctions.deselectScripts(doc);
+ swapFunctions.swapRootAttributes(doc);
+
+ // Keep the elements created by `@emotion/cache`
+ const emotionElements = document.querySelectorAll(`style[${EMOTION_ATTR}]`);
+ swapHeadElements(doc, Array.from(emotionElements));
+
+ const restoreFocusFunction = swapFunctions.saveFocus();
+ swapFunctions.swapBodyElement(doc.body, document.body);
+ restoreFocusFunction();
+}
+
+/**
+ * This function is a copy of the original `swapHeadElements` function from `astro:transitions/client`.
+ * The difference is that you can pass a list of elements that should not be removed
+ * in the new document.
+ *
+ * See https://github.com/withastro/astro/blob/d6f17044d3873df77cfbc73230cb3194b5a7d82a/packages/astro/src/transitions/swap-functions.ts#L51
+ */
+function swapHeadElements(doc: Document, ignoredElements: Element[]) {
+ for (const el of Array.from(document.head.children)) {
+ const newEl = persistedHeadElement(el, doc);
+
+ if (newEl) {
+ newEl.remove();
+ } else {
+ if (!ignoredElements.includes(el)) {
+ el.remove();
+ }
+ }
+ }
+
+ document.head.append(...doc.head.children);
+}
+
+function persistedHeadElement(el: Element, newDoc: Document) {
+ const id = el.getAttribute(PERSIST_ATTR);
+ const newEl = id && newDoc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
+ if (newEl) {
+ return newEl;
+ }
+ if (el.matches('link[rel=stylesheet]')) {
+ const href = el.getAttribute('href');
+ return newDoc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
+ }
+ return null;
+}
diff --git a/packages/astro/tsup.config.ts b/packages/astro/tsup.config.ts
index 608373d9937..e453f9b1a11 100644
--- a/packages/astro/tsup.config.ts
+++ b/packages/astro/tsup.config.ts
@@ -24,6 +24,6 @@ export default defineConfig(() => {
bundle: true,
sourcemap: true,
format: ['esm'],
- external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage'],
+ external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage', 'astro:transitions/client'],
};
});