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
transitionConfig.vue
8 Replies
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:
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:
----------------------<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai
/middleware/loading.global.js
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?
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:
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).
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:
----------------------<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@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 completeLet 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:
2. Modify the finishAnimation
function to delay the page transition:
3. In your app.vue
, wrap the NuxtPage
component with a Transition
component:
4. Use the useLoadingIndicator
composable in your CustomLoader.vue
to manage the loading state:
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:
----------------------<@759827985745969153> Kapa.ai is still learning and improving, please let me know how I did by reacting below.