---
url: /guide/dynamic-layout.md
---
# 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 [src/router.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 [src/pages/settings.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](#layout-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](/guide/#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 renders `panel` without 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: false` are 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 and `setPageLayout` target 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` / `beforeRouteLeave` declared *inside a layout component* never runs; a
  warning is logged once per layout name when this is detected. Use `onBeforeRouteUpdate` / `onBeforeRouteLeave`
  (Composition API) or a router-level guard instead - these still work correctly.
* **Vue < 3.3:** `app.runWithContext` doesn't exist yet, so the wrapper's `beforeRouteEnter` can't reach the router to
  preload a lazy layout, and the global `beforeResolve` fallback is only installed once a wrapper's `setup()` has run
  at least once. In practice this only matters for a lazy layout selected by a *page's own* `beforeRouteEnter` during
  the *initial* navigation: it renders once its chunk loads instead of being awaited by the navigation. Static `meta.layout`,
  `<route>` blocks, `beforeEach`, route-level `beforeEnter`, and any navigation after the first are unaffected.
* `useLayout()` returns the *requested* name - `meta.layout`, or the last value passed to `setPageLayout` - not the
  rendered fallback. So for an unknown name it still reports that name, even though the wrapper renders
  `defaultLayout` (and warns). This matches Nuxt's `useLayout` semantics.
* Switching layouts remounts the layout subtree. If you use a `<transition>` keyed on the route (see
  [Common Patterns](/guide/patterns#transitions)), an in-place `setPageLayout` does not change the key, so no transition
  runs.
* These helpers are also exported from `vite-plugin-vue-layouts-next/runtime` if 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 `setPageLayout` in a component's setup: the override would leak into the routes pre-rendered after it.
  Use `meta.layout` or 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`, a `setPageLayout` called there is not cleared until a wrapper has mounted once.
* Layouts may be any component, including a bare functional component. If you build a `layouts` map by hand for
  `createLayoutWrapper`, wrap `() => import()` factories with `lazyLayout()` from `vite-plugin-vue-layouts-next/runtime`
  so they aren't mistaken for a synchronous component.
* The `RouteMeta` augmentation (`layout?: string | false | { name?, props? }`, `layoutProps?`, `isLayout?`) ships in
  `vite-plugin-vue-layouts-next/runtime` and reaches you through `client.d.ts`, which imports types via the package's
  `exports`. This requires `moduleResolution: "bundler"` (or `node16`/`nodenext`) in `tsconfig.json` - the Vite default.
