Dynamic Layouts
Since v3.1, the layout of a page is resolved when it renders, not when the router is created. That makes three things possible: changing the layout per navigation from a router guard, switching it in place with setPageLayout, and reading the active layout with useLayout.
Per-navigation: router guards
setupLayouts still wraps each page in a parent route, but that parent reads route.meta.layout on every navigation. So a guard can decide the layout, for example based on a user role:
ts
router.beforeEach((to) => {
if (to.path.startsWith('/admin'))
to.meta.layout = user.role === 'admin' ? 'admin' : 'default'
})meta.layout from the page's <route> block is the default; the guard only needs to assign when it wants to override.
In place: setPageLayout
vue
<script setup lang="ts">
import { setPageLayout } from 'virtual:generated-layouts'
</script>
<template>
<button @click="setPageLayout('focus')">
Focus mode
</button>
<button @click="setPageLayout(false)">
No layout
</button>
</template>- Takes a layout name or
false(render the page without a layout), and optionally the props for that layout. - The override lasts until the router navigates to a different
path. Query or hash changes keep it. - It can be called before the router is ready; the first page then renders with that layout.
Layout props
Both ways of changing the layout at runtime can also pass props to it (see Passing Props to a Layout for the static form):
ts
// In place
setPageLayout('panel', { title: 'Settings', sidebar: false })
// Per navigation: assign the object form, or only `layoutProps` to keep the layout
router.beforeEach((to) => {
to.meta.layout = { name: 'panel', props: { title: String(to.name) } }
// or: to.meta.layoutProps = { title: String(to.name) }
})- A layout set at runtime brings its own props:
setPageLayout('panel')without props renderspanelwithout the props the page declared statically. - Like the layout name, runtime props only apply to the innermost layout; each outer level keeps the props of its own page. Props of different levels are never merged.
useLayout()still returns only the layout name.
Reading it: useLayout
vue
<script setup lang="ts">
import { useLayout } from 'virtual:generated-layouts'
const layout = useLayout() // ComputedRef<string | false>
</script>Notes
- Pages with a static
layout: falseare never wrapped, so they cannot be given a layout at runtime. - In a nested route (a page with children, each level with its own
layout), guards andsetPageLayouttarget the innermost layout - the one directly around the leaf page. The static layouts of the outer levels are unaffected. - An unknown layout name logs a warning and falls back to
defaultLayout. - Layouts are rendered by a shared wrapper component, not matched as route components, so an Options-API
beforeRouteEnter/beforeRouteUpdate/beforeRouteLeavedeclared inside a layout component never runs; a warning is logged once per layout name when this is detected. UseonBeforeRouteUpdate/onBeforeRouteLeave(Composition API) or a router-level guard instead - these still work correctly. - Vue < 3.3:
app.runWithContextdoesn't exist yet, so the wrapper'sbeforeRouteEntercan't reach the router to preload a lazy layout, and the globalbeforeResolvefallback is only installed once a wrapper'ssetup()has run at least once. In practice this only matters for a lazy layout selected by a page's ownbeforeRouteEnterduring the initial navigation: it renders once its chunk loads instead of being awaited by the navigation. Staticmeta.layout,<route>blocks,beforeEach, route-levelbeforeEnter, and any navigation after the first are unaffected. useLayout()returns the requested name -meta.layout, or the last value passed tosetPageLayout- not the rendered fallback. So for an unknown name it still reports that name, even though the wrapper rendersdefaultLayout(and warns). This matches Nuxt'suseLayoutsemantics.- Switching layouts remounts the layout subtree. If you use a
<transition>keyed on the route (see Common Patterns), an in-placesetPageLayoutdoes not change the key, so no transition runs. - These helpers are also exported from
vite-plugin-vue-layouts-next/runtimeif you need them outside the virtual module. - The override is module-level state. During SSG pre-rendering (vite-ssg creates a fresh app per route in one process) do not call
setPageLayoutin a component's setup: the override would leak into the routes pre-rendered after it. Usemeta.layoutor a router guard instead. - The override is cleared by a guard that the layout wrapper installs on first mount. If the app's first route has a static
layout: false, asetPageLayoutcalled there is not cleared until a wrapper has mounted once. - Layouts may be any component, including a bare functional component. If you build a
layoutsmap by hand forcreateLayoutWrapper, wrap() => import()factories withlazyLayout()fromvite-plugin-vue-layouts-next/runtimeso they aren't mistaken for a synchronous component. - The
RouteMetaaugmentation (layout?: string | false | { name?, props? },layoutProps?,isLayout?) ships invite-plugin-vue-layouts-next/runtimeand reaches you throughclient.d.ts, which imports types via the package'sexports. This requiresmoduleResolution: "bundler"(ornode16/nodenext) intsconfig.json- the Vite default.