diff --git a/docs/1.getting-started/5.transitions.md b/docs/1.getting-started/5.transitions.md
index e79115bd72a1..75e4af6ef767 100644
--- a/docs/1.getting-started/5.transitions.md
+++ b/docs/1.getting-started/5.transitions.md
@@ -426,6 +426,35 @@ export default defineNuxtConfig({
})
```
+The possible values are: `false`, `true`, or `'always'`.
+
+If set to true, Nuxt will not apply transitions if the user's browser matches `prefers-reduced-motion: reduce` (recommended). If set to `always`, Nuxt will always apply the transition and it is up to you to respect the user's preference.
+
+By default, view transitions are enabled for all [pages](/docs/guide/directory-structure/pages), but you can set a different global default.
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+ app: {
+ // Disable view transitions globally, and opt-in on a per page basis
+ viewTransition: false
+ },
+})
+```
+
+It is possible to override the default `viewTransition` value for a page by setting the `viewTransition` key in [`definePageMeta`](/docs/api/utils/define-page-meta) of the page:
+
+```vue [pages/about.vue]
+
+```
+
+::alert{type="warning"}
+Overriding view transitions on a per-page basis will only have an effect if you have enabled the `experimental.viewTransition` option.
+::
+
If you are also using Vue transitions like `pageTransition` and `layoutTransition` (see above) to achieve the same result as the new View Transitions API, then you may wish to _disable_ Vue transitions if the user's browser supports the newer, native web API. You can do this by creating `~/middleware/disable-vue-transitions.global.ts` with the following contents:
```js
diff --git a/docs/3.api/3.utils/define-page-meta.md b/docs/3.api/3.utils/define-page-meta.md
index bbcb5fed8e46..074229ae3649 100644
--- a/docs/3.api/3.utils/define-page-meta.md
+++ b/docs/3.api/3.utils/define-page-meta.md
@@ -33,6 +33,7 @@ interface PageMeta {
alias?: string | string[]
pageTransition?: boolean | TransitionProps
layoutTransition?: boolean | TransitionProps
+ viewTransition?: boolean | 'always'
key?: false | string | ((route: RouteLocationNormalizedLoaded) => string)
keepalive?: boolean | KeepAliveProps
layout?: false | LayoutKey | Ref | ComputedRef
@@ -104,6 +105,14 @@ interface PageMeta {
Set name of the transition to apply for current page. You can also set this value to `false` to disable the page transition.
+ **`viewTransition`**
+
+ - **Type**: `boolean | 'always'`
+
+ **Experimental feature, only available when [enabled in your nuxt.config file](/docs/getting-started/transitions#view-transitions-api-experimental)**
+ Enable/disable View Transitions for the current page.
+ If set to true, Nuxt will not apply the transition if the users browser matches `prefers-reduced-motion: reduce` (recommended). If set to `always`, Nuxt will always apply the transition.
+
**`redirect`**
- **Type**: [`RouteRecordRedirectOption`](https://router.vuejs.org/guide/essentials/redirect-and-alias.html#redirect-and-alias)
diff --git a/packages/nuxt/src/app/plugins/view-transitions.client.ts b/packages/nuxt/src/app/plugins/view-transitions.client.ts
index 949ba73614ff..bb3a696d2ddd 100644
--- a/packages/nuxt/src/app/plugins/view-transitions.client.ts
+++ b/packages/nuxt/src/app/plugins/view-transitions.client.ts
@@ -2,10 +2,12 @@ import { isChangingPage } from '../components/utils'
import { useRouter } from '../composables/router'
import { defineNuxtPlugin } from '../nuxt'
// @ts-expect-error virtual file
-import { viewTransition } from '#build/nuxt.config.mjs'
+import { appViewTransition as defaultViewTransition } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin((nuxtApp) => {
- if (!document.startViewTransition || (viewTransition !== 'always' && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) { return }
+ if (!document.startViewTransition) {
+ return
+ }
let finishTransition: undefined | (() => void)
let abortTransition: undefined | (() => void)
@@ -13,9 +15,14 @@ export default defineNuxtPlugin((nuxtApp) => {
const router = useRouter()
router.beforeResolve((to, from) => {
- if (!isChangingPage(to, from)) {
+ const viewTransitionMode = to.meta.viewTransition ?? defaultViewTransition
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
+ const prefersNoTransition = prefersReducedMotion && viewTransitionMode !== 'always'
+
+ if (viewTransitionMode === false || prefersNoTransition || !isChangingPage(to, from)) {
return
}
+
const promise = new Promise((resolve, reject) => {
finishTransition = resolve
abortTransition = reject
diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts
index 8732d83308ad..bf4375113b85 100644
--- a/packages/nuxt/src/pages/module.ts
+++ b/packages/nuxt/src/pages/module.ts
@@ -1,6 +1,6 @@
import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises'
-import { addBuildPlugin, addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, updateTemplates, useNitro } from '@nuxt/kit'
+import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, updateTemplates, useNitro } from '@nuxt/kit'
import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import { joinURL } from 'ufo'
@@ -473,6 +473,26 @@ export default defineNuxtModule({
}
})
+
+ // add page meta types if enabled
+ if (nuxt.options.experimental.viewTransition) {
+ addTypeTemplate({
+ filename: 'types/view-transitions.d.ts',
+ getContents: ({ nuxt }) => {
+ const runtimeDir = resolve(distDir, 'pages/runtime')
+ const composablesFile = relative(join(nuxt.options.buildDir, 'types'), resolve(runtimeDir, 'composables'))
+ return [
+ 'import { ComputedRef, MaybeRef } from \'vue\'',
+ `declare module ${genString(composablesFile)} {`,
+ ' interface PageMeta {',
+ ` viewTransition?: boolean | 'always'`,
+ ' }',
+ '}',
+ ].join('\n')
+ }
+ })
+ }
+
// Add
addComponent({
name: 'NuxtPage',
diff --git a/packages/schema/src/config/app.ts b/packages/schema/src/config/app.ts
index db831cee5eca..461c76b2c8fc 100644
--- a/packages/schema/src/config/app.ts
+++ b/packages/schema/src/config/app.ts
@@ -145,6 +145,20 @@ export default defineUntypedSchema({
*/
pageTransition: false,
+ /**
+ * Default values for view transitions.
+ *
+ * This only has an effect when **experimental** support for View Transitions is
+ * [enabled in your nuxt.config file](/docs/getting-started/transitions#view-transitions-api-experimental).
+ *
+ * This can be overridden with `definePageMeta` on an individual page.
+ * @see https://nuxt.com/docs/getting-started/transitions#view-transitions-api-experimental
+ * @type {typeof import('../src/types/config').NuxtAppConfig['viewTransition']}
+ */
+ viewTransition: {
+ $resolve: async (val, get) => val ?? await get('experimental.viewTransition') ?? false
+ },
+
/**
* Default values for KeepAlive configuration between pages.
*
diff --git a/packages/schema/src/types/config.ts b/packages/schema/src/types/config.ts
index df925a10277c..aa8197d299c0 100644
--- a/packages/schema/src/types/config.ts
+++ b/packages/schema/src/types/config.ts
@@ -144,6 +144,7 @@ export interface NuxtAppConfig {
head: AppHeadMetaObject
layoutTransition: boolean | TransitionProps
pageTransition: boolean | TransitionProps
+ viewTransition?: boolean | 'always'
keepalive: boolean | KeepAliveProps
}