[{"data":1,"prerenderedAt":58},["ShallowReactive",2],{"post-page-transitions-that-dont-fight-your-data":3,"blog-posts":14},{"_path":4,"title":5,"description":6,"date":7,"tags":8,"readingTime":12,"body":13},"\u002Fblog\u002Fpage-transitions-that-dont-fight-your-data\u002F","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.","2026-07-30",[9,10,11],"frontend","vue","ux",6,"# Page Transitions That Don't Fight Your Data\n\nA 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.\n\n## Treat navigation as a state change, not a magic wipe\n\nIn 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.\n\nStart by naming what changes on navigation:\n\n- **Shell**: header, nav, layout chrome (usually stays)\n- **Page region**: the part that swaps per route\n- **Data**: lists, detail records, form defaults (often arrives after the route)\n\nAnimate the page region, not the whole app. Keep the shell stable so orientation survives the transition. If your layout uses a shared `\u003Cmain>` wrapper, that is the boundary, not `body` or `#__nuxt`.\n\nIn Nuxt 3, a minimal pattern is a keyed outlet inside the layout:\n\n```vue\n\u003C!-- layouts\u002Fdefault.vue -->\n\u003Ctemplate>\n  \u003CAppHeader \u002F>\n  \u003Cmain id=\"main-content\">\n    \u003CNuxtPage :transition=\"{ name: 'page', mode: 'out-in' }\" \u002F>\n  \u003C\u002Fmain>\n  \u003CAppFooter \u002F>\n\u003C\u002Ftemplate>\n```\n\n`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.\n\n## Keep the outgoing page until the next one is ready\n\nThe 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.\n\nBetter defaults:\n\n- **Prefetch on intent**: hover or focus on nav links when routes are cheap to warm (`NuxtLink` prefetches by default in many setups; use it deliberately for heavy routes)\n- **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\n- **Skeleton inside the incoming slot**: if you must enter early, show a layout-shaped placeholder in the page region, not a blank fade\n\nFor data that must load after navigation, coordinate transition timing with your fetch:\n\n```vue\n\u003Cscript setup>\nconst route = useRoute();\nconst { data, pending } = await useAsyncData(\n  () => `post-${route.params.slug}`,\n  () => $fetch(`\u002Fapi\u002Fposts\u002F${route.params.slug}`),\n  { watch: [() => route.params.slug] }\n);\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle v-if=\"!pending && data\">\n    \u003Ch1>{{ data.title }}\u003C\u002Fh1>\n    \u003C!-- … -->\n  \u003C\u002Farticle>\n  \u003CPostSkeleton v-else \u002F>\n\u003C\u002Ftemplate>\n```\n\nPair 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*.\n\nFor 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.\n\n## Match motion to the wait you are asking for\n\nShort, 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.\n\nRough pairing:\n\n| Situation | Transition approach |\n|-----------|---------------------|\n| Static \u002F prerendered route | Short fade or slide (150 to 250ms) on the page region |\n| Client fetch under ~500ms | Same, plus inline pending UI in the destination |\n| Slower or uncertain fetch | Top progress bar or skeleton; defer leave animation |\n| Modal \u002F drawer | Focus trap + enter\u002Fexit on the overlay, not the whole page |\n\nRespect `prefers-reduced-motion`. Wrap decorative keyframes in a media query and keep instant swaps as the fallback:\n\n```css\n@media (prefers-reduced-motion: no-preference) {\n  .page-enter-active,\n  .page-leave-active {\n    transition: opacity 0.2s ease;\n  }\n  .page-enter-from,\n  .page-leave-to {\n    opacity: 0;\n  }\n}\n```\n\nVue’s `\u003CTransition>` 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.\n\nAlso 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\u002Fforward when you can; continuity should work both ways.\n\n## What to skip when transitions cause more harm\n\nNot every route deserves animation. Skip or simplify transitions when:\n\n- **Forms are mid-edit**: confirm before leaving; do not slide away half-filled fields without warning\n- **Lists are being filtered**: updating query params in place often needs no page transition at all\n- **Errors are likely**: a dramatic exit into an error boundary feels punitive; keep the shell calm\n- **Performance is tight**: low-end phones pay for blur, scale, and staggered children; one opacity tween beats five\n\nAnti-patterns worth deleting:\n\n- Global transitions on every nested child route (nested outlets fighting each other)\n- `out-in` on the full layout when only a paragraph changed\n- Animations longer than the fetch they are masking; users learn to distrust the polish\n\nIf 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.\n\n## Wrap-up\n\nPage 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.",[15,25,27,35,43,51],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":24},"\u002Fblog\u002Fwhy-indie-games-are-breaking-through\u002F","Why Indie Games Are Breaking Through","Small teams are outselling big studios on their best weeks. Here is what actually changed: distribution, community, and what players reward now.","2026-08-07",[21,22,23],"gaming","indie","culture",5,{"_path":4,"title":5,"description":6,"date":7,"tags":26,"readingTime":12},[9,10,11],{"_path":28,"title":29,"description":30,"date":31,"tags":32,"readingTime":24},"\u002Fblog\u002Fclient-side-tools-that-earn-trust\u002F","Client-Side Tools That Earn Trust","How to design browser tools that feel private by default: local processing, clear data boundaries, and UX that never asks people to guess where their files go.","2026-07-20",[33,9,34],"privacy","tools",{"_path":36,"title":37,"description":38,"date":39,"tags":40,"readingTime":24},"\u002Fblog\u002Fenvironment-variables-for-static-sites\u002F","Environment Variables for Static Sites","How to use build-time env vars in Nuxt and other static generators without leaking secrets or breaking CI.","2026-07-18",[41,42,9],"nuxt","devops",{"_path":44,"title":45,"description":46,"date":47,"tags":48,"readingTime":24},"\u002Fblog\u002Faccessible-forms-people-finish\u002F","Accessible Forms That People Actually Finish","Practical accessibility checks for labels, errors, and focus so your forms work for more users.","2026-07-15",[49,50,9],"accessibility","html",{"_path":52,"title":53,"description":54,"date":55,"tags":56,"readingTime":24},"\u002Fblog\u002Fshipping-static-sites-with-nuxt\u002F","Shipping Static Sites with Nuxt","How I build and deploy a Nuxt site to Cloudflare Pages with predictable routes and content.","2026-07-07",[41,57,42],"ssg",1786156741314]