Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): controllable view transition #25264

Merged
merged 11 commits into from Jan 29, 2024
29 changes: 29 additions & 0 deletions docs/1.getting-started/5.transitions.md
Expand Up @@ -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]
<script setup lang="ts">
definePageMeta({
viewTransition: false
})
</script>
```

::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
Expand Down
9 changes: 9 additions & 0 deletions docs/3.api/3.utils/define-page-meta.md
Expand Up @@ -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<LayoutKey> | ComputedRef<LayoutKey>
Expand Down Expand Up @@ -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)**</br>
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)
Expand Down
13 changes: 10 additions & 3 deletions packages/nuxt/src/app/plugins/view-transitions.client.ts
Expand Up @@ -2,20 +2,27 @@ 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)

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<void>((resolve, reject) => {
finishTransition = resolve
abortTransition = reject
Expand Down
22 changes: 21 additions & 1 deletion 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'
Expand Down Expand Up @@ -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 <NuxtPage>
addComponent({
name: 'NuxtPage',
Expand Down
14 changes: 14 additions & 0 deletions packages/schema/src/config/app.ts
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions packages/schema/src/types/config.ts
Expand Up @@ -144,6 +144,7 @@ export interface NuxtAppConfig {
head: AppHeadMetaObject
layoutTransition: boolean | TransitionProps
pageTransition: boolean | TransitionProps
viewTransition?: boolean | 'always'
keepalive: boolean | KeepAliveProps
}

Expand Down