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 }