I'm building a custom curtain-style page transition with Astro's ClientRouter. I've disabled all default view transitions on the <html> element and handle everything manually in a persistent <PageTransition> component.
The curtain animation and the page fetch run at different speeds. Sometimes the page loads before the curtain finishes, sometimes the other way around. I need the DOM swap to only happen once the curtain fully covers the screen.
My solution is to intercept event.loader in astro:before-preparation and replace it with a Promise.all that waits for both the original fetch AND the curtain animation
my shortend scripts...
<html transition:animate="none">
<head>
<ClientRouter />
</head>
<body >
<StickyHeader />
<main>
<slot />
</main>
<Footer />
<PageTransition />
</body>
</html>
let enterPromise = null;
// 1. Navigation starts → begin curtain animation, wrap loader
document.addEventListener('astro:before-preparation', (e) => {
enterPromise = new Promise((resolve) => {
// curtain slides in... when done:
setTimeout(resolve, 800); // simulate animation
});
// Wrap the loader so swap waits for BOTH fetch + curtain
const originalLoader = e.loader;
e.loader = async () => {
await Promise.all([originalLoader(), enterPromise]);
};
});
// After swap: wait, then animate curtain out
document.addEventListener('astro:page-load', async () => {
if (!enterPromise) return; // first load, skip
await enterPromise; // safety — should already be resolved
await new Promise((r) => setTimeout(r, 600)); // hold delay
// curtain slides out...
enterPromise = null;
});
My questions:
Is intercepting event.loader the recommended way to delay the swap, or is there a better approach?
Any concerns with this pattern in general?