Mastering layouts in Vue.js is a milestone that separates beginners from intermediate developers. When you first start, you likely wrap every page in a and component. It works, but it’s repetitive and destroys component state every time you navigate.
This guide explores “Smart Layouts”, a set of patterns that make your Vue application more maintainable, performant, and capable of advanced behaviors like persistent state (think Spotify’s audio player that doesn’t stop when you change pages).
Level 1: The “Wrapper” Anti-Pattern
Most developers start here. As expert Vue.js developers know, you create a DefaultLayout.vue and manually wrap every single page content with it
Home Page
The Problem:
Every time you navigate from Home to About, the DefaultLayout component is destroyed and re-created.
- Performance Hit: The sidebar, navigation, and footer re-render unnecessarily.
- State Loss: If you have a search bar in the header, the text clears on navigation.
- Scroll Position: If your sidebar has a scroll position, it resets to the top.
Level 2: The “Smart Layout” Pattern (The Gold Standard)
The solution is to decouple the layout from the page. We want the Router to tell App.vue which layout to use, and App.vue handles the swapping.
Step 1: Define Layouts in Route Meta
In your router configuration, add a meta field to specify the layout.
// src/router/index.js
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
const routes = [
{
path: '/',
component: Home,
meta: { layout: 'AppLayout' } // Custom layout
},
{
path: '/login',
component: Login,
meta: { layout: 'SimpleLayout' } // No sidebar/footer
}
]
Step 2: The Dynamic Layout Component
In App.vue, we use Vue’s feature. We calculate which layout to show based on the current route.
:is="currentLayout">
/>
Why this rocks:
When you navigate between two pages that share the AppLayout, Vue does not unmount the layout component. It sees that the parent component (AppLayout) hasn’t changed, so it only updates the slot content (). Your header search bar and sidebar scroll position remain intact!
Level 3: Persistent Layouts (“The Spotify Effect”)
If you are building a media app or a complex dashboard, you might notice that sometimes your layout still re-renders if you aren’t careful.
The Golden Rule of Persistence:
Never put a :key="$route.fullPath" on your Layout component.
BAD (Destroys Layout on every route change):
:is="currentLayout" :key="$route.fullPath">
/>
GOOD (Preserves Layout):
:is="currentLayout">
v-slot="{ Component }">
:is="Component" :key="$route.fullPath" />
By moving the key inside the layout (onto the routed component itself), the Layout stays stable, but the page content refreshes properly.
Level 4: Communicating Between Page and Layout
A common challenge: “How do I change the text in the Header (Layout) from the Page?”
Option A: Teleport (The Cleanest UI Hack)
Vue’s feature allows a child component (Page) to send content to a DOM node in the parent (Layout).
-
Layout: Add a target div.
id="header-actions"> -
Page: Teleport buttons or text into that div.
to="#header-actions"> Profile Content...
Option B: Use a Store (Pinia)
For logical state (e.g., showing a loading spinner in the Layout), use Pinia.
// src/stores/ui.js
export const useUIStore = defineStore('ui', {
state: () => ({ headerTitle: 'Default Title' }),
actions: { setTitle(title) { this.headerTitle = title } }
})
In the Page:
import { useUIStore } from '@/stores/ui'
const ui = useUIStore()
ui.setTitle('User Settings')
In the Layout:
{{ ui.headerTitle }}
Level 5: Smooth Transitions
Want to cross-fade between layouts? This is tricky because, when working with dynamic layouts with Vue JSX, you are transitioning the wrapper, not just the inner content.
Wrap your dynamic component in a Transition with mode="out-in" to ensure the old layout leaves before the new one enters.
v-slot="{ Component, route }">
name="fade" mode="out-in">
:is="currentLayout" :key="currentLayout">
:is="Component" :key="route.fullPath" />
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
Summary Checklist
- Stop wrapping pages manually. Use
route.meta.layout. - Use Dynamic Components (
) inApp.vue. - Preserve State by avoiding keys on the Layout component itself.
- Teleport page specific actions into the Layout’s header/sidebar.
- Use Pinia for global UI state like titles or loading bars.