Vue.js 59 🟢 Route Meta Fields, Scrolling Behavior Customization, and View Transitions API Integration
A Vue Router configuration is not just a list of paths and components. It is a place to attach arbitrary data to each route—whether a page requires authentication, what transition animation it should use, or what title to set on the document. That data lives in the meta field, and it is accessible from the route object and from every navigation guard . The scroll behavior function is the mechanism that decides where the page should be positioned after navigation—at the top, at an anchor, or at the position the user left when they pressed the back button . And the View Transitions API is the browser’s native mechanism for animating between two states of the document, which Vue Router can integrate with to produce smooth route transitions without a third-party animation library.
These three features are often mentioned together because they share a common theme: controlling the user’s experience of navigation beyond simply rendering the correct component. The meta field is the data layer—it tells the guards and the template what kind of route this is. The scroll behavior is the positioning layer—it decides where the viewport lands after the route changes. The View Transitions API is the animation layer—it decides how the old view visually becomes the new view.
Key point: meta fields attach arbitrary data to a route record and are merged from parent to child in route.meta; scrollBehavior returns a position descriptor that controls where the page scrolls after navigation; and the View Transitions API can wrap route changes in a native browser animation by calling document.startViewTransition() around the router’s navigation.
Why meta fields are more than arbitrary data
The merged record problem. A route like /posts/new matches both the parent record (/posts) and the child record (new). If the parent has meta: { requiresAuth: true } and the child has meta: { section: 'posts' }, the route object’s meta field is a non-recursive merge of both: { requiresAuth: true, section: 'posts' } . This means a guard can check to.meta.requiresAuth without iterating through to.matched and checking each record individually. The merge is shallow—if both parent and child define the same key, the child’s value wins—and it is not recursive, so nested objects are replaced, not merged.
The TypeScript problem. By default, route.meta is typed as Record<string | number | symbol, unknown>, which means every access requires a type assertion. The fix is to extend the RouteMeta interface from vue-router with the fields the application actually uses . This makes route.meta.requiresAuth a typed boolean and route.meta.transition a typed string, and it catches typos at compile time rather than at runtime.
The guard access problem. Navigation guards receive the to and from route objects, which means to.meta is available in beforeEach, beforeEnter, and onBeforeRouteLeave. This is what makes the meta field the standard place for authentication requirements, role checks, and transition names—the data is defined once on the route and read by every guard that needs it .
a. Meta fields in practice
The meta field is defined as a plain object on the route record. The fields are application-specific: requiresAuth, role, transition, title, or anything else the guards and templates need.
// router.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// Only authenticated users can create posts
meta: { requiresAuth: true, title: 'New Post' },
},
{
path: ':id',
component: PostsDetail,
// Anybody can read a post
meta: { requiresAuth: false, title: 'Post Detail' },
},
],
},
],
})
The guard reads the merged meta field directly. The to.meta object contains the child’s fields merged over the parent’s, so a single check covers both levels .
// Global guard reading meta
router.beforeEach((to, from) => {
// to.meta is the merged meta from all matched records
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
return {
path: '/login',
// Preserve the intended destination for after login
query: { redirect: to.fullPath },
}
}
})
For TypeScript, the RouteMeta interface is extended with the fields the application uses. The declare module block must be in a file that is included in the project’s tsconfig.json .
// types/router.d.ts
import 'vue-router'
export {}
declare module 'vue-router' {
interface RouteMeta {
// Optional: not every route requires auth
requiresAuth?: boolean
// Optional: used by the transition wrapper
transition?: string
// Optional: used by the document title guard
title?: string
}
}
b. Scroll behavior customization
The scrollBehavior function is provided when creating the router. It receives to, from, and savedPosition, and returns a position descriptor or a Promise that resolves to one . The savedPosition is only available on popstate navigation—when the user presses the browser’s back or forward button .
The most common configuration scrolls to the top on new routes, restores the saved position on back/forward navigation, and scrolls to the anchor if the URL contains a hash.
// router.js
const router = createRouter({
history: createWebHistory(),
routes: [...],
scrollBehavior(to, from, savedPosition) {
// 1. Back/forward navigation: restore the saved position
if (savedPosition) {
return savedPosition
}
// 2. Hash present: scroll to the anchor element
if (to.hash) {
return {
el: to.hash,
// Offset by 80px for a fixed header
top: 80,
// Smooth scrolling if the browser supports it
behavior: 'smooth',
}
}
// 3. Default: scroll to the top of the page
return { top: 0 }
},
})
The el field accepts a CSS selector or a DOM element. The top and left values are treated as offsets relative to that element . A positive top moves the scroll position above the element, which is what a fixed header requires.
If the scroll should wait for a page transition to finish, the function can return a Promise. The router waits for the Promise to resolve before scrolling .
scrollBehavior(to, from, savedPosition) {
// Wait for the transition to finish before scrolling
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 })
}, 400)
})
}
c. View Transitions API integration
The View Transitions API is a browser feature that captures a snapshot of the current document, runs a DOM update, and animates between the two snapshots. It is available in Chrome 111+, Edge 111+, Safari 18+, and Firefox 133+ . Vue Router does not have built-in View Transitions support in the stable release, but the integration is straightforward: wrap the router’s navigation in document.startViewTransition().
The key is that startViewTransition() accepts a callback that performs the DOM update. The callback must return a Promise that resolves when the update is complete. For a router navigation, that means awaiting router.push().
// A helper that wraps navigation in a view transition
async function navigateWithTransition(router, to) {
// Fallback for browsers without the API
if (!document.startViewTransition) {
return router.push(to)
}
// Start the transition; the callback performs the navigation
const transition = document.startViewTransition(async () => {
// Await the navigation so the DOM is updated before the snapshot
await router.push(to)
// Wait for Vue to flush the DOM updates
await nextTick()
})
// Wait for the transition to finish
await transition.finished
}
The nextTick() call is important. Vue’s DOM updates are asynchronous, and the View Transitions API captures the new snapshot immediately after the callback resolves. Without nextTick(), the snapshot may capture the old DOM before Vue has rendered the new component .
The CSS side of the View Transitions API uses the ::view-transition-old() and ::view-transition-new() pseudo-elements to define the animation. A simple crossfade is the default, but a slide animation can be defined with @keyframes and the view-transition-name property.
/* Default crossfade */
::view-transition-old(root) {
animation: fade-out 200ms ease;
}
::view-transition-new(root) {
animation: fade-in 200ms ease;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
The integration can be combined with the meta field’s transition value to choose different animations per route, but the CSS pseudo-elements are global. Per-route animation control requires assigning a view-transition-name to the element that changes and defining separate keyframes for that name.
Complete Example Session
The following session builds a router with typed meta fields, a scroll behavior that handles top, anchor, and saved position, and a navigation helper that uses the View Transitions API.
// ============================================
// PART 1: TYPED META FIELDS
// ============================================
// types/router.d.ts
import 'vue-router'
export {}
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
transition?: string
}
}
// ============================================
// PART 2: ROUTES WITH META
// ============================================
// router.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Home,
meta: { title: 'Home', transition: 'fade' },
},
{
path: '/dashboard',
component: Dashboard,
// Requires auth and uses a slide transition
meta: { requiresAuth: true, title: 'Dashboard', transition: 'slide' },
},
{
path: '/docs',
component: Docs,
// Has anchors; scroll behavior will handle them
meta: { title: 'Docs', transition: 'fade' },
},
],
// ============================================
// PART 3: SCROLL BEHAVIOR
// ============================================
scrollBehavior(to, from, savedPosition) {
// Restore position on back/forward
if (savedPosition) return savedPosition
// Scroll to anchor with offset for fixed header
if (to.hash) {
return { el: to.hash, top: 80, behavior: 'smooth' }
}
// Default: top of page
return { top: 0 }
},
})
// ============================================
// PART 4: AUTH GUARD READING META
// ============================================
router.beforeEach((to, from) => {
// Read the merged meta field
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
// ============================================
// PART 5: DOCUMENT TITLE FROM META
// ============================================
router.afterEach((to) => {
// Set the title from the meta field
if (to.meta.title) {
document.title = `${to.meta.title} | My App`
}
})
// ============================================
// PART 6: VIEW TRANSITION NAVIGATION HELPER
// ============================================
// navigation.js
import { nextTick } from 'vue'
export async function navigateWithTransition(router, to) {
// Fallback: no View Transitions API
if (!document.startViewTransition) {
return router.push(to)
}
// Start the transition and perform the navigation inside it
const transition = document.startViewTransition(async () => {
// Navigate to the target route
await router.push(to)
// Wait for Vue to render the new component into the DOM
await nextTick()
})
// Resolve when the animation completes
await transition.finished
}
/* ============================================
PART 7: VIEW TRANSITION STYLES
============================================ */
::view-transition-old(root) {
animation: fade-out 200ms ease;
}
::view-transition-new(root) {
animation: fade-in 200ms ease;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
The seven parts cover the typed meta interface, the route definitions with meta, the scroll behavior with three cases, the auth guard reading meta, the title guard, the View Transitions navigation helper, and the CSS animation. What the session demonstrates is that meta is the data that guards and templates read, scrollBehavior is the positioning policy, and the View Transitions API is the animation layer—and all three can coexist without conflicting.
Quick Reference
Meta Fields
| Aspect | Detail |
|---|---|
| Definition | meta: { key: value } on route record |
| Access | route.meta.key (merged from parent to child) |
| Merge behavior | Non-recursive, child overrides parent |
| TypeScript | Extend RouteMeta interface |
| Guard access | to.meta in beforeEach/beforeEnter |
Scroll Behavior Return Values
| Return | Effect |
|---|---|
{ top: 0 } | Scroll to top |
{ el: '#id', top: 80 } | Scroll to element with offset |
savedPosition | Restore history position |
{ el: to.hash } | Scroll to anchor |
null / {} | No scrolling |
Promise<position> | Delayed scrolling |
View Transitions API
| Aspect | Detail |
|---|---|
| Entry point | document.startViewTransition(callback) |
| Callback | Performs DOM update, returns Promise |
| Vue requirement | await nextTick() after navigation |
| CSS hooks | ::view-transition-old/new(root) |
| Browser support | Chrome 111+, Safari 18+, Firefox 133+ |
Best Practices
✅ Do This:
// Read merged meta in guards
if (to.meta.requiresAuth && !auth.isLoggedIn()) return '/login'
// Restore saved position on back/forward
if (savedPosition) return savedPosition
// Wait for Vue to flush before the new snapshot
await router.push(to)
await nextTick()
❌ Don’t Do This:
// Iterating matched records when meta is already merged
to.matched.some(r => r.meta.requiresAuth) // unnecessary
// Scrolling to top on every navigation, including back
return { top: 0 } // ignores savedPosition
// Forgetting nextTick in View Transition callback
await router.push(to) // snapshot may capture old DOM
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Meta field undefined | Not declared on any matched record | Define on parent or child |
| Scroll not restoring | savedPosition ignored | Check it first |
| Anchor hidden behind header | No offset | top: 80 in scrollBehavior |
| View Transition captures old DOM | Missing nextTick() | Await nextTick() after push |
| TypeScript error on meta access | RouteMeta not extended | Add module declaration |
Real-World Examples
1. Auth meta
{ path: '/admin', meta: { requiresAuth: true } }
2. Title meta
{ path: '/', meta: { title: 'Home' } }
3. Transition meta
{ path: '/settings', meta: { transition: 'slide' } }
4. Scroll to top
scrollBehavior() { return { top: 0 } }
5. Scroll to anchor with offset
scrollBehavior(to) {
if (to.hash) return { el: to.hash, top: 80 }
}
6. Restore saved position
scrollBehavior(to, from, saved) {
if (saved) return saved
}
7. View transition helper
async function go(router, to) {
if (!document.startViewTransition) return router.push(to)
await document.startViewTransition(async () => {
await router.push(to)
await nextTick()
}).finished
}
8. TypeScript meta
declare module 'vue-router' {
interface RouteMeta { requiresAuth?: boolean }
}
9. Document title
router.afterEach((to) => {
document.title = to.meta.title || 'App'
})
10. View transition CSS
::view-transition-old(root) { animation: fade-out 200ms; }
::view-transition-new(root) { animation: fade-in 200ms; }
Visual
Meta Merge
┌──────────────────────────────────────────────┐
│ Parent: /posts │
│ meta: { requiresAuth: true } │
│ │ │
│ ▼ │
│ Child: new │
│ meta: { section: 'posts' } │
│ │ │
│ ▼ │
│ route.meta = { │
│ requiresAuth: true, │
│ section: 'posts' │
│ } │
│ │
│ Child overrides parent on same key │
└──────────────────────────────────────────────┘
Scroll Behavior Decision
┌──────────────────────────────────────────────┐
│ savedPosition? │
│ ├─ YES → return savedPosition (back/forward)│
│ └─ NO → to.hash? │
│ ├─ YES → return { el: hash } │
│ └─ NO → return { top: 0 } │
└──────────────────────────────────────────────┘
View Transition Flow
┌──────────────────────────────────────────────┐
│ startViewTransition(async () => { │
│ await router.push(to) │
│ await nextTick() │
│ }) │
│ │ │
│ ▼ │
│ Snapshot old DOM │
│ │ │
│ ▼ │
│ Run callback (navigation + render) │
│ │ │
│ ▼ │
│ Snapshot new DOM │
│ │ │
│ ▼ │
│ Animate between snapshots │
└──────────────────────────────────────────────┘
Title Guard
┌──────────────────────────────────────────────┐
│ router.afterEach((to) => { │
│ if (to.meta.title) { │
│ document.title = to.meta.title │
│ } │
│ }) │
│ │
│ Navigation completes → title updates │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Meta definition | meta: { key: value } on route |
| Meta access | route.meta.key |
| Meta merge | Non-recursive, child overrides parent |
| TypeScript | Extend RouteMeta interface |
| Scroll behavior | scrollBehavior(to, from, savedPosition) |
| Scroll to top | { top: 0 } |
| Scroll to anchor | { el: to.hash, top: offset } |
| Saved position | return savedPosition |
| View Transitions | document.startViewTransition() |
| Vue requirement | await nextTick() after navigation |
Key takeaways:
- Meta fields are the route’s data layer. They are defined on the route record, merged from parent to child, and accessible in every guard and in the component. Authentication requirements, transition names, and document titles all belong in meta .
- The merge is non-recursive.
to.metacontains the child’s fields over the parent’s, but nested objects are replaced, not merged. Access the field directly instead of iteratingto.matched. - Scroll behavior has three primary cases. Back/forward restores the saved position, a hash scrolls to the anchor with an optional offset, and everything else scrolls to the top .
- The View Transitions API requires a DOM flush. The callback passed to
startViewTransitionmust awaitrouter.push()andnextTick()before it resolves, or the new snapshot will capture the old DOM . - Browser support is incomplete. Chrome and Edge support View Transitions, Safari 18+ and Firefox 133+ are catching up, and the fallback is a plain navigation without animation .
- TypeScript typing prevents runtime errors. Extending
RouteMetamakesroute.meta.requiresAutha typed boolean androute.meta.transitiona typed string, catching typos at compile time .
Remember: Meta fields, scroll behavior, and View Transitions are three layers of navigation experience. Meta is the data that tells the guards and templates what kind of route this is. Scroll behavior is the positioning policy that decides where the user lands after the route changes. View Transitions is the animation layer that makes the change feel continuous. Each is configured separately, and each has a specific place in the router’s configuration. The meta field is the most flexible because it holds arbitrary data, the scroll behavior is the most visible because it determines what the user sees first, and the View Transitions API is the most modern because it uses the browser’s native animation capabilities instead of a JavaScript animation library.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!