Page Transitions That Dont Fight Your Data
Route animations feel polished when they respect loading time, preserve context, and get out of the way for people who prefer less motion.
Page Transitions That Don't Fight Your Data
A smooth page transition promises continuity. A slow fetch with a flashy fade promises confusion. The goal is not motion for its own sake—it is helping people understand that navigation started, that the app is still working, and that the new view belongs to the same place.
Treat navigation as a state change, not a magic wipe
In a multi-page site, the browser handles the handoff: old document out, new document in. In an SPA, you own that handoff. If you animate the entire viewport away before new content exists, users stare at an empty stage.
Start by naming what changes on navigation:
- Shell — header, nav, layout chrome (usually stays)
- Page region — the part that swaps per route
- Data — lists, detail records, form defaults (often arrives after the route)
Animate the page region, not the whole app. Keep the shell stable so orientation survives the transition. If your layout uses a shared <main> wrapper, that is the boundary—not body or #__nuxt.
In Nuxt 3, a minimal pattern is a keyed outlet inside the layout:
<!-- layouts/default.vue -->
<template>
<AppHeader />
<main id="main-content">
<NuxtPage :transition="{ name: 'page', mode: 'out-in' }" />
</main>
<AppFooter />
</template>
mode: 'out-in' runs leave-then-enter, which is readable when both views are ready. It is painful when the incoming page still needs a fetch—more on that below. The habit to build first: decide what is transitioning and what is not, then design loading around that boundary.
Keep the outgoing page until the next one is ready
The most common transition mistake is animating away content that still represents the truth. Someone clicks “Posts,” the blog list vanishes in 200ms, and a spinner sits in a fading container for two seconds. That feels slower than no animation at all.
Better defaults:
- Prefetch on intent — hover or focus on nav links when routes are cheap to warm (
NuxtLinkprefetches by default in many setups; use it deliberately for heavy routes) - Hold the old view — delay the leave transition until critical data for the next route resolves, or keep the previous page visible with a lightweight progress indicator at the top
- Skeleton inside the incoming slot — if you must enter early, show a layout-shaped placeholder in the page region, not a blank fade
For data that must load after navigation, coordinate transition timing with your fetch:
<script setup>
const route = useRoute();
const { data, pending } = await useAsyncData(
() => `post-${route.params.slug}`,
() => $fetch(`/api/posts/${route.params.slug}`),
{ watch: [() => route.params.slug] }
);
</script>
<template>
<article v-if="!pending && data">
<h1>{{ data.title }}</h1>
<!-- … -->
</article>
<PostSkeleton v-else />
</template>
Pair this with CSS that does not animate opacity on an empty node. The transition should fire when there is something meaningful to show—or when a skeleton clearly marks “loading here.” Same lesson as honest loading states: the animation answers where am I going? not please wait in a void.
For shared elements (a card thumbnail expanding into a hero), reach for view transitions only when the DOM path is predictable and the benefit beats the maintenance cost. Most apps ship faster with a simple fade on the page region plus good skeletons.
Match motion to the wait you are asking for
Short, subtle transitions work when navigation is mostly synchronous—static prerendered pages, cached data, or instant client-side filters. Longer or more elaborate motion belongs to moments where content is already present.
Rough pairing:
| Situation | Transition approach |
|---|---|
| Static / prerendered route | Short fade or slide (150–250ms) on the page region |
| Client fetch under ~500ms | Same, plus inline pending UI in the destination |
| Slower or uncertain fetch | Top progress bar or skeleton; defer leave animation |
| Modal / drawer | Focus trap + enter/exit on the overlay, not the whole page |
Respect prefers-reduced-motion. Wrap decorative keyframes in a media query and keep instant swaps as the fallback:
@media (prefers-reduced-motion: no-preference) {
.page-enter-active,
.page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
}
Vue’s <Transition> and Nuxt’s page transitions both honor this if you gate the CSS. Do not rely on motion alone to communicate state—screen readers and motion-sensitive users still need text or structure that says navigation happened.
Also watch scroll position. A beautiful fade loses trust if the new page loads scrolled to the middle of a long article because the previous scroll position leaked. Reset scroll on route change for full-page swaps (scrollBehavior in Vue Router, or window.scrollTo on afterEach when appropriate). Keep scroll restoration for back/forward when you can—continuity should work both ways.
What to skip when transitions cause more harm
Not every route deserves animation. Skip or simplify transitions when:
- Forms are mid-edit — confirm before leaving; do not slide away half-filled fields without warning
- Lists are being filtered — updating query params in place often needs no page transition at all
- Errors are likely — a dramatic exit into an error boundary feels punitive; keep the shell calm
- Performance is tight — low-end phones pay for blur, scale, and staggered children; one opacity tween beats five
Anti-patterns worth deleting:
- Global transitions on every nested child route (nested outlets fighting each other)
out-inon the full layout when only a paragraph changed- Animations longer than the fetch they are masking—users learn to distrust the polish
If you are unsure, ship without a route transition first. Add motion when you can point at a specific confusion it fixes (“people did not realize the settings page changed”). Measurement beats taste: if time-to-interactive regresses, cut the effect.
Wrap-up
Page transitions earn their place when they respect data timing and human perception. Keep the shell steady, animate the page region—not the whole world—hold or skeleton the outgoing view until the next one has something to show, scale motion to the wait, and fall back cleanly for reduced motion and slow devices. Continuity is the product; the easing curve is optional garnish.