Nuxt 3 GSAP custom page transitions

i'm having a little problem about nuxt 3 page transition hooks. onBeforeLeave doesn't fire until redirected page loads. problem is i'm using useAsyncData for my profile page. And page transition doesnt start until that function completed, so user still wait in the same page. here is the source codes: app.vue
<template>
<NuxtLayout>
<NuxtPage :transition="pageTransition"></NuxtPage>
</NuxtLayout>
<div id="transitionLayer" class="w-full h-0 overflow-hidden fixed top-0 left-0 bg-black z-[100] flex items-center justify-center">
<div class="text-white text-6xl font-bold"><span id="transitionProgress">0</span>%</div>
</div>
</template>

<script setup>
import pageTransition from './utils/transitionConfig.js';
</script>
<template>
<NuxtLayout>
<NuxtPage :transition="pageTransition"></NuxtPage>
</NuxtLayout>
<div id="transitionLayer" class="w-full h-0 overflow-hidden fixed top-0 left-0 bg-black z-[100] flex items-center justify-center">
<div class="text-white text-6xl font-bold"><span id="transitionProgress">0</span>%</div>
</div>
</template>

<script setup>
import pageTransition from './utils/transitionConfig.js';
</script>
transitionConfig.vue
import gsap from 'gsap';

export default {
name: 'page-transition',
mode: 'out-in',
onEnter: (el, done) => {
console.log('enter');
done();
},
onBeforeLeave: el => {
console.log(el);
console.log('before leave');
const transitionLayer = document.getElementById('transitionLayer');
gsap.set(transitionLayer, {
height: 0,
top: 'auto',
bottom: 0
});
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
},
onLeave: (el, done) => {
console.log('leave');
const transitionLayer = document.getElementById('transitionLayer');

gsap
.timeline({
delay: 1,
onStart: () => {
done();
gsap.set(transitionLayer, {
top: 0,
bottom: 'auto'
});
}
})
.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut'
});
}
};
import gsap from 'gsap';

export default {
name: 'page-transition',
mode: 'out-in',
onEnter: (el, done) => {
console.log('enter');
done();
},
onBeforeLeave: el => {
console.log(el);
console.log('before leave');
const transitionLayer = document.getElementById('transitionLayer');
gsap.set(transitionLayer, {
height: 0,
top: 'auto',
bottom: 0
});
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
},
onLeave: (el, done) => {
console.log('leave');
const transitionLayer = document.getElementById('transitionLayer');

gsap
.timeline({
delay: 1,
onStart: () => {
done();
gsap.set(transitionLayer, {
top: 0,
bottom: 'auto'
});
}
})
.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut'
});
}
};
8 Replies
kapa.ai
kapa.ai2w ago
Hi @Yazılım Panteri. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Based on the information provided in the knowledge sources, I can offer some insights into your issue with Nuxt 3 page transitions and GSAP. The problem you're experiencing, where the onBeforeLeave hook doesn't fire until the redirected page loads, is likely related to how Nuxt 3 handles page transitions and data fetching. In Nuxt 3, when navigating between pages, the URL changes before the data for the new page has finished fetching. This behavior differs from Nuxt 2, where the URL would only change after all data fetching was complete Github Issue (https://github.com/nuxt/nuxt/issues/29535). This change in behavior can cause issues when using page transitions, especially when combined with useAsyncData for data fetching. To address this issue, you have a few options: 1. Use the Suspense component: Nuxt 3 uses Vue's Suspense component to handle async data. However, this might not solve your specific issue with transitions Github Issue (https://github.com/nuxt/nuxt/issues/29535). 2. Consider using the onBeforeRouteLeave composable: This allows you to register a route guard within a component, which might give you more control over when the transition starts Nuxt Docs (https://nuxt.com/docs/api/utils/on-before-route-leave). 3. Implement a custom solution: You could create a global middleware to handle the transition timing. Here's an example of how you might do this:
// ~/middleware/transition-handler.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Start your transition here
// You can access your GSAP animation logic here
// Return a promise that resolves when your transition is complete
})
// ~/middleware/transition-handler.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Start your transition here
// You can access your GSAP animation logic here
// Return a promise that resolves when your transition is complete
})
4. Use the experimental View Transitions API: Nuxt 3 has experimental support for the View Transitions API, which might provide a more seamless way to handle transitions Nuxt Docs (https://nuxt.com/docs/api/nuxt-config#viewtransition-1). However, be aware that this is still experimental and may have some limitations. It's worth noting that there are known issues with View Transitions and data fetching. The Nuxt documentation states: "If you perform data fetching within your page setup functions, that you may wish to reconsider using this feature for the moment. (By design, View Transitions completely freeze DOM updates whilst they are taking place.)" Nuxt Docs (https://nuxt.com/docs/getting-started/transitions#known-issues). Given the complexity of your setup with GSAP and custom transition logic, you might need to experiment with these approaches to find the best solution for your specific use case. You may also want to consider simplifying your transition logic or moving some of the heavy lifting to the client-side to avoid conflicts with server-side rendering and data fetching. Remember, the Nuxt team is aware of these transition-related issues and is working on improvements. Keep an eye on the Nuxt GitHub repository for updates and potential solutions in future releases. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Yazılım Panteri
@kapa.ai /middleware/loading.global.js
// middleware/transition.js

import gsap from 'gsap';

export default defineNuxtRouteMiddleware(async (to, from, next) => {
const app = useNuxtApp();
if (!document) {
return;
}

// Access the transition layer
const transitionLayer = document.getElementById('transitionLayer');

// Start the page loading and trigger transition animation
app.hook('page:loading:start', () => {
console.log('Loading started');
// Trigger GSAP animation to show the transition layer
gsap.set(transitionLayer, { height: 0, top: 'auto', bottom: 0 });
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
});

// End the page loading and trigger transition animation for the exit
app.hook('page:finish', () => {
console.log('Loading ended');
// Trigger GSAP animation to hide the transition layer after loading
gsap.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut',
onComplete: () => {
// Ensure the transition layer is reset after the animation
gsap.set(transitionLayer, { top: 'auto', bottom: 0 });
}
});
});
});
// middleware/transition.js

import gsap from 'gsap';

export default defineNuxtRouteMiddleware(async (to, from, next) => {
const app = useNuxtApp();
if (!document) {
return;
}

// Access the transition layer
const transitionLayer = document.getElementById('transitionLayer');

// Start the page loading and trigger transition animation
app.hook('page:loading:start', () => {
console.log('Loading started');
// Trigger GSAP animation to show the transition layer
gsap.set(transitionLayer, { height: 0, top: 'auto', bottom: 0 });
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
});

// End the page loading and trigger transition animation for the exit
app.hook('page:finish', () => {
console.log('Loading ended');
// Trigger GSAP animation to hide the transition layer after loading
gsap.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut',
onComplete: () => {
// Ensure the transition layer is reset after the animation
gsap.set(transitionLayer, { top: 'auto', bottom: 0 });
}
});
});
});
i'm using nested pages sometimes and page:finish hook called multiple times when that happen. for exapmle i have pages/profile.vue and also have pages/profile/[name]/index.vue. is there any prevent or fix to call page:finish hook multiple times for same request?
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the information provided in the knowledge sources, I can offer some insights into your issue with the page:finish hook being called multiple times for nested routes in Nuxt 3. The problem you're experiencing is likely related to how Nuxt 3 handles nested routes and page transitions. When using nested routes, the page:finish hook might indeed be triggered multiple times, once for each nested component that is loaded. To address this issue, you have a few options: 1. Use a flag to prevent multiple executions: You can introduce a flag to ensure that the transition animation only runs once per navigation. Here's how you might modify your middleware:
let isTransitioning = false;

export default defineNuxtRouteMiddleware(async (to, from, next) => {
const app = useNuxtApp();
if (!document) {
return;
}

const transitionLayer = document.getElementById('transitionLayer');

app.hook('page:loading:start', () => {
if (!isTransitioning) {
isTransitioning = true;
console.log('Loading started');
gsap.set(transitionLayer, { height: 0, top: 'auto', bottom: 0 });
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
}
});

app.hook('page:finish', () => {
if (isTransitioning) {
console.log('Loading ended');
gsap.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut',
onComplete: () => {
gsap.set(transitionLayer, { top: 'auto', bottom: 0 });
isTransitioning = false;
}
});
}
});
});
let isTransitioning = false;

export default defineNuxtRouteMiddleware(async (to, from, next) => {
const app = useNuxtApp();
if (!document) {
return;
}

const transitionLayer = document.getElementById('transitionLayer');

app.hook('page:loading:start', () => {
if (!isTransitioning) {
isTransitioning = true;
console.log('Loading started');
gsap.set(transitionLayer, { height: 0, top: 'auto', bottom: 0 });
gsap.to(transitionLayer, {
height: '100%',
duration: 1,
ease: 'power2.out'
});
}
});

app.hook('page:finish', () => {
if (isTransitioning) {
console.log('Loading ended');
gsap.to(transitionLayer, {
height: 0,
duration: 0.5,
ease: 'power2.inOut',
onComplete: () => {
gsap.set(transitionLayer, { top: 'auto', bottom: 0 });
isTransitioning = false;
}
});
}
});
});
2. Use the page:transition:finish hook: Instead of page:finish, you could use the page:transition:finish hook, which is called after the page transition is complete. This might help avoid multiple calls for nested routes Stack Overflow (https://stackoverflow.com/questions/77541639).
app.hook('page:transition:finish', () => {
console.log('Page transition finished');
// Your GSAP animation code here
});
app.hook('page:transition:finish', () => {
console.log('Page transition finished');
// Your GSAP animation code here
});
3. Consider using the experimental View Transitions API: Nuxt 3 has experimental support for the View Transitions API, which might provide a more seamless way to handle transitions, especially for nested routes Nuxt Docs (https://nuxt.com/docs/api/nuxt-config#viewtransition-1). However, be aware that this is still experimental and may have some limitations. 4. Use the pages:extend hook: If you want to apply middleware to specific pages or nested routes, you can use the pages:extend hook in your nuxt.config.ts file to programmatically add middleware to certain pages Nuxt Docs (https://nuxt.com/docs/guide/directory-structure/middleware#example). Remember that working with transitions in Nuxt 3, especially with nested routes, can be complex. You might need to experiment with these approaches to find the best solution for your specific use case. Also, keep an eye on the Nuxt GitHub repository for updates and potential solutions in future releases, as the Nuxt team is continuously working on improving these aspects of the framework. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Yazılım Panteri
@kapa.ai CustomLoader.vue <template> <div class="indicator fixed top-0 left-0" :style="{ pointerEvents: 'none', width: '100%', height: ${props.height}px, opacity: isLoading ? 1 : 0, background: error ? props.errorColor : props.color || undefined, backgroundSize: '100% auto', // Always full size transform: scaleX(${Math.max(progress, 10)}%), // Ensure minimum width transformOrigin: 'left', transition: 'transform 0.1s, height 0.4s, opacity 0.4s', zIndex: 999999 }"></div> <div id="transitionLayer" class="fixed left-0 w-full bg-black z-[10000] flex flex-col items-center justify-start h-0 bottom-0 overflow-hidden" ref="transitionLayer"></div> </template> <script setup> import { ref, watch, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import gsap from 'gsap'; // Props const props = defineProps({ throttle: { type: Number, default: 200 }, duration: { type: Number, default: 2000 }, height: { type: Number, default: 3 // Adjust height for thicker loader }, color: { type: String, default: 'repeating-linear-gradient(to right,#00dc82 0%,#34cdfe 50%,#0047e1 100%)' }, errorColor: { type: String, default: 'repeating-linear-gradient(to right,#f87171 0%,#ef4444 100%)' }, estimatedProgress: { type: Function, default: (duration, elapsed) => (2 / Math.PI) * 100 * Math.atan(((elapsed / duration) * 100) / 50) } }); // Refs const transitionLayer = ref(null); const animationComplete = ref(false); // Tracks animation state // Loading Indicator const { progress, isLoading, error, start, finish, clear } = useLoadingIndicator({ duration: props.duration, throttle: props.throttle, estimatedProgress: props.estimatedProgress }); // Router instance const router = useRouter(); // Watch for loading state watch(progress, newVal => { if (newVal === 100) { startAnimation(); } }); // Start Animation function startAnimation() { gsap.set(transitionLayer.value, { height: 0, top: 'auto', bottom: 0 }); gsap.to(transitionLayer.value, { height: '100%', duration: 1, ease: 'power2.out', onComplete: () => { finishAnimation(); } }); } // Finish Animation function finishAnimation() { const appContainer = document.getElementById('app'); const navbar = document.getElementById('navbar'); const tl = gsap.timeline({ onComplete: () => { // Remove GSAP-applied styles gsap.set(appContainer, { clearProps: 'all' }); } }); animationComplete.value = false; gsap.set(transitionLayer.value, { height: '100%', top: 0, bottom: 'auto' }); tl.to(transitionLayer.value, { height: 0, top: '-100%', duration: 1, ease: 'power2.out' }); tl.fromTo( appContainer, { yPercent: 100 }, { yPercent: 0, duration: 0.8, ease: 'power2.out' }, '<+=0.1' ); tl.play(); } // Mount hook to ensure transition layer is set up onMounted(() => { animationComplete.value = false; }); </script> <style scoped> #transitionLayer { will-change: height; } .counter-enter-active, .counter-leave-active { transition: all 0.15s ease-out; } .counter-leave-active { position: absolute; } .counter-enter-from { transform: translateY(-300%); } .counter-enter-to { transform: translateY(0%); } .counter-leave-from { transform: translateY(0%); } .counter-leave-to { transform: translateY(300%); } </style> app.vue <template> <CustomLoader></CustomLoader> <NuxtLayout> <NuxtPage></NuxtPage> </NuxtLayout> </template> <script setup></script> i want to make a custom page loader. new page should mount after finishAnimation starts. important point is new page should continue to it's loading progress. just not affect visually until animation complete
kapa.ai
kapa.ai7d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the information provided in the knowledge sources and your custom loader implementation, I can offer some suggestions to achieve the behavior you're looking for with a custom page loader in Nuxt 3. To make the new page mount after finishAnimation starts, while allowing it to continue its loading progress in the background, you can leverage Nuxt's lifecycle hooks and the useLoadingIndicator composable. Here's how you can modify your approach: 1. Use the page:start and page:finish hooks to control your custom loader: In your CustomLoader.vue, you can use these hooks to manage the loading state and animations:
import { useNuxtApp } from '#app'

const nuxtApp = useNuxtApp()

nuxtApp.hook('page:start', () => {
start() // Start the loading indicator
})

nuxtApp.hook('page:finish', () => {
finish() // Finish the loading indicator
})
import { useNuxtApp } from '#app'

const nuxtApp = useNuxtApp()

nuxtApp.hook('page:start', () => {
start() // Start the loading indicator
})

nuxtApp.hook('page:finish', () => {
finish() // Finish the loading indicator
})
2. Modify the finishAnimation function to delay the page transition:
function finishAnimation() {
const appContainer = document.getElementById('app');
const tl = gsap.timeline({
onComplete: () => {
// Remove GSAP-applied styles
gsap.set(appContainer, { clearProps: 'all' });
nuxtApp.callHook('page:transition:finish') // Signal that the transition is complete
}
});

// ... rest of your animation code ...

tl.play();
}
function finishAnimation() {
const appContainer = document.getElementById('app');
const tl = gsap.timeline({
onComplete: () => {
// Remove GSAP-applied styles
gsap.set(appContainer, { clearProps: 'all' });
nuxtApp.callHook('page:transition:finish') // Signal that the transition is complete
}
});

// ... rest of your animation code ...

tl.play();
}
3. In your app.vue, wrap the NuxtPage component with a Transition component:
<template>
<CustomLoader />
<NuxtLayout>
<Transition name="page" mode="out-in">
<NuxtPage />
</Transition>
</NuxtLayout>
</template>

<script setup>
import { useNuxtApp } from '#app'

const nuxtApp = useNuxtApp()
// Delay page transition until animation is complete
nuxtApp.hook('page:transition:finish', () => {
// This will be called when your custom animation is complete
// You can add any additional logic here if needed
})
</script>

<style>
.page-enter-active,
.page-leave-active {
transition: opacity 0.5s;
}
.page-enter,
.page-leave-to {
opacity: 0;
}
</style>
<template>
<CustomLoader />
<NuxtLayout>
<Transition name="page" mode="out-in">
<NuxtPage />
</Transition>
</NuxtLayout>
</template>

<script setup>
import { useNuxtApp } from '#app'

const nuxtApp = useNuxtApp()
// Delay page transition until animation is complete
nuxtApp.hook('page:transition:finish', () => {
// This will be called when your custom animation is complete
// You can add any additional logic here if needed
})
</script>

<style>
.page-enter-active,
.page-leave-active {
transition: opacity 0.5s;
}
.page-enter,
.page-leave-to {
opacity: 0;
}
</style>
4. Use the useLoadingIndicator composable in your CustomLoader.vue to manage the loading state:
const { progress, isLoading, error, start, finish } = useLoadingIndicator({
duration: props.duration,
throttle: props.throttle,
estimatedProgress: props.estimatedProgress
})
const { progress, isLoading, error, start, finish } = useLoadingIndicator({
duration: props.duration,
throttle: props.throttle,
estimatedProgress: props.estimatedProgress
})
This approach allows the new page to continue loading in the background while your custom animation plays. The page transition will only occur after your custom animation is complete, thanks to the page:transition:finish hook. Remember that this solution might need some fine-tuning based on your specific requirements and the complexity of your pages. You may need to adjust the timing of the animations and hooks to get the exact behavior you're looking for. Also, keep in mind that Nuxt 3 has experimental support for the View Transitions API, which might provide a more seamless way to handle transitions in the future Nuxt Docs (https://nuxt.com/docs/api/nuxt-config#viewtransition-1). However, as it's still experimental, the approach outlined above should work well for your current needs. Lastly, be aware that complex custom loaders and transitions can potentially impact performance, especially on slower devices. Always test your implementation thoroughly to ensure a smooth user experience across different devices and network conditions. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai7d ago
<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Want results from more Discord servers?
Add your server