过渡动画

使用 Vue 或原生浏览器 View Transitions API 在页面和布局之间应用过渡动画。

Nuxt 利用 Vue 的 <Transition> 组件来在页面和布局之间应用过渡动画。

因为 Nuxt 使用 Vue 的 <Transition> 组件,要做动画的页面或布局必须有单个根元素。有多个根元素(片段)的页面或布局无法做动画,过渡不会执行,路由切换时可能报错。Nuxt 会在开发环境警告你。把模板包裹在单个根元素中(比如一个 <div>)。

页面过渡

你可以启用页面过渡,为所有页面自动应用过渡动画。

export default defineNuxtConfig({
  app: {
    pageTransition: { name: 'page', mode: 'out-in' },
  },
})

如果你同时切换布局和页面,这里设置的页面过渡不会执行。应该设置布局过渡。

要开始添加页面间过渡,在 app.vue 中添加以下 CSS:

<template>
  <NuxtPage />
</template>

<style>
.page-enter-active,
.page-leave-active {
  transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
  opacity: 0;
  filter: blur(1rem);
}
</style>
<template>
  <div>
    <h1>首页</h1>
    <NuxtLink to="/about">关于页面</NuxtLink>
  </div>
</template>
<template>
  <div>
    <h1>关于页面</h1>
    <NuxtLink to="/">首页</NuxtLink>
  </div>
</template>

页面间导航时的效果:

要为某个页面设置不同的过渡,在页面的 definePageMeta 中设置 pageTransition:

<script setup lang="ts">
definePageMeta({
  pageTransition: {
    name: 'rotate',
  },
})
</script>
<template>
  <NuxtPage />
</template>

<style>
/* ... */
.rotate-enter-active,
.rotate-leave-active {
  transition: all 0.4s;
}
.rotate-enter-from,
.rotate-leave-to {
  opacity: 0;
  transform: rotate3d(1, 1, 1, 15deg);
}
</style>

跳转到关于页面时会有 3D 旋转效果:

布局过渡

你可以启用布局过渡,为所有布局自动应用过渡动画。

export default defineNuxtConfig({
  app: {
    layoutTransition: { name: 'layout', mode: 'out-in' },
  },
})

要开始添加页面和布局间的过渡,在 app.vue 中添加以下 CSS:

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<style>
.layout-enter-active,
.layout-leave-active {
  transition: all 0.4s;
}
.layout-enter-from,
.layout-leave-to {
  filter: grayscale(1);
}
</style>
<template>
  <div>
    <pre>默认布局</pre>
    <slot />
  </div>
</template>

<style scoped>
div {
  background-color: lightgreen;
}
</style>
<template>
  <div>
    <pre>橙色布局</pre>
    <slot />
  </div>
</template>

<style scoped>
div {
  background-color: #eebb90;
  padding: 20px;
  height: 100vh;
}
</style>
<template>
  <div>
    <h1>首页</h1>
    <NuxtLink to="/about">关于页面</NuxtLink>
  </div>
</template>
<script setup lang="ts">
definePageMeta({
  layout: 'orange',
})
</script>

<template>
  <div>
    <h1>关于页面</h1>
    <NuxtLink to="/">首页</NuxtLink>
  </div>
</template>

页面间导航时的效果:

和 pageTransition 类似,你可以用 definePageMeta 为页面组件设置自定义 layoutTransition:

<script setup lang="ts">
definePageMeta({
  layout: 'orange',
  layoutTransition: {
    name: 'slide-in',
  },
})
</script>

全局配置

你可以在 nuxt.config 中全局自定义默认过渡名称。

pageTransition 和 layoutTransition 都接受 TransitionProps 作为 JSON 可序列化值,你可以传入 name、mode 和其他自定义 CSS 过渡的合法 transition-props。

export default defineNuxtConfig({
  app: {
    pageTransition: {
      name: 'fade',
      mode: 'out-in', // 默认
    },
    layoutTransition: {
      name: 'slide',
      mode: 'out-in', // 默认
    },
  },
})

如果你修改了 name 属性,CSS 类名也要相应修改。

要覆盖全局过渡属性,使用 definePageMeta 为单个 Nuxt 页面定义页面或布局过渡,覆盖 nuxt.config 中全局定义的过渡。

<script setup lang="ts">
definePageMeta({
  pageTransition: {
    name: 'bounce',
    mode: 'out-in', // 默认
  },
})
</script>

禁用过渡

pageTransition 和 layoutTransition 可以为特定路由禁用:

<script setup lang="ts">
definePageMeta({
  pageTransition: false,
  layoutTransition: false,
})
</script>

或者在 nuxt.config 中全局禁用:

export default defineNuxtConfig({
  app: {
    pageTransition: false,
    layoutTransition: false,
  },
})

JavaScript Hooks

对于进阶场景,你可以使用 JavaScript hooks 为 Nuxt 页面创建高度动态和自定义的过渡。

这种方式非常适合配合 GSAP 等 JavaScript 动画库使用。

<script setup lang="ts">
definePageMeta({
  pageTransition: {
    name: 'custom-flip',
    mode: 'out-in',
    onBeforeEnter: (el) => {
      console.log('进入前...')
    },
    onEnter: (el, done) => {},
    onAfterEnter: (el) => {},
  },
})
</script>

了解更多 Transition 组件可用的 JavaScript hooks。

动态过渡

要使用条件逻辑应用动态过渡,你可以利用内联中间件为 to.meta.pageTransition 分配不同的过渡名称。

<script setup lang="ts">
definePageMeta({
  pageTransition: {
    name: 'slide-right',
    mode: 'out-in',
  },
  middleware (to, from) {
    if (to.meta.pageTransition && typeof to.meta.pageTransition !== 'boolean') {
      to.meta.pageTransition.name = +to.params.id! > +from.params.id! ? 'slide-left' : 'slide-right'
    }
  },
})
</script>

<template>
  <h1>#{{ $route.params.id }}</h1>
</template>

<style>
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
  transition: all 0.2s;
}
.slide-left-enter-from {
  opacity: 0;
  transform: translate(50px, 0);
}
.slide-left-leave-to {
  opacity: 0;
  transform: translate(-50px, 0);
}
.slide-right-enter-from {
  opacity: 0;
  transform: translate(-50px, 0);
}
.slide-right-leave-to {
  opacity: 0;
  transform: translate(50px, 0);
}
</style>
<script setup lang="ts">
const route = useRoute()
const id = computed(() => Number(route.params.id || 1))
const prev = computed(() => '/' + (id.value - 1))
const next = computed(() => '/' + (id.value + 1))
</script>

<template>
  <div>
    <slot />
    <div v-if="$route.params.id">
      <NuxtLink :to="prev">⬅️</NuxtLink> |
      <NuxtLink :to="next">➡️</NuxtLink>
    </div>
  </div>
</template>

现在跳转到下一个 id 时应用 slide-left 过渡,跳到上一个时应用 slide-right:

NuxtPage 过渡

当 app.vue 中使用 <NuxtPage /> 时,可以通过 transition prop 配置全局过渡。

<template>
  <div>
    <NuxtLayout>
      <NuxtPage
        :transition="{
          name: 'bounce',
          mode: 'out-in',
        }"
      />
    </NuxtLayout>
  </div>
</template>

注意,这个页面过渡不能被单个页面上的 definePageMeta 覆盖。

View Transitions API(实验性)

Nuxt 内置了 View Transitions API(见 MDN)的实验性实现。这是一种令人兴奋的原生浏览器过渡新方式,它(尤其)能够在不同页面的不相关元素之间做过渡。

你可以在 StackBlitz 上看演示。

在配置文件中启用 experimental.viewTransition 选项:

export default defineNuxtConfig({
  experimental: {
    viewTransition: true,
  },
})

可选值:false、true 或 'always'。

设为 true 时,如果用户浏览器匹配 prefers-reduced-motion: reduce,Nuxt 不会应用过渡(推荐)。设为 always 时,Nuxt 始终应用过渡,由你自己决定是否尊重用户偏好。

默认情况下,所有页面都启用 view transitions,但你可以设置不同的全局默认值。

export default defineNuxtConfig({
  app: {
    // 全局禁用 view transitions,按页面单独启用
    viewTransition: false,
  },
})

可以在页面的 definePageMeta 中设置 viewTransition 键来覆盖页面的默认值:

<script setup lang="ts">
definePageMeta({
  viewTransition: false,
})
</script>

按页面覆盖 view transitions 只有在你启用了 experimental.viewTransition 选项时才生效。

View Transition 类型 v4.4

View transition 类型允许你根据导航类型应用不同的 CSS 动画。这对于创建不对称过渡很有用(比如前进和后退用不同动画)。

类型设置在 ViewTransition 上,可以在 CSS 中用 :active-view-transition-type() 伪类选择器定位。

你可以在 nuxt.config.ts 中全局设置默认类型:

export default defineNuxtConfig({
  app: {
    viewTransition: {
      enabled: true,
      types: ['slide'],
    },
  },
})

或者用 definePageMeta 按页面配置类型。按页面的类型支持静态数组和动态函数:

<script setup lang="ts">
definePageMeta({
  viewTransition: {
    enabled: true,
    // 涉及此页面的任何过渡都会应用的类型
    types: ['slide'],
    // 仅导航到此页面时应用的类型
    toTypes: ['slide-in'],
    // 仅从此页面导航离开时应用的类型
    fromTypes: ['slide-out'],
  },
})
</script>

你也可以在 definePageMeta 中使用函数作为 types、toTypes 和 fromTypes 的值,根据路由动态确定类型:

<script setup lang="ts">
definePageMeta({
  viewTransition: {
    enabled: true,
    toTypes: (to, from) => {
      // 跳转到更高 ID 时向左滑,否则向右
      return Number(to.params.id) > Number(from.params.id)
        ? ['slide-left']
        : ['slide-right']
    },
  },
})
</script>

然后在 CSS 中定位这些类型:

/* 默认交叉淡入 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}

/* 向左滑动动画 */
html:active-view-transition-type(slide-left) {
  &::view-transition-old(root) {
    animation: slide-out-left 0.3s ease-in-out;
  }
  &::view-transition-new(root) {
    animation: slide-in-right 0.3s ease-in-out;
  }
}

/* 向右滑动动画 */
html:active-view-transition-type(slide-right) {
  &::view-transition-old(root) {
    animation: slide-out-right 0.3s ease-in-out;
  }
  &::view-transition-new(root) {
    animation: slide-in-left 0.3s ease-in-out;
  }
}

types、toTypes 和 fromTypes 的函数值只在 definePageMeta 中有效,不能在 nuxt.config.ts 中使用(那里只支持静态 string[])。

page:view-transition:start hook 提供对 ViewTransition 对象的访问,它包含一个 types 属性(ViewTransitionTypeSet),可以在运行时读取或修改:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:view-transition:start', (transition) => {
    // 运行时读取或修改类型
    console.log([...transition.types])
  })
})

如果你同时在使用 Vue 过渡(比如 pageTransition 和 layoutTransition)来实现和新的 View Transitions API 相同的效果,那么当用户浏览器支持更新的原生 Web API 时,你可能希望禁用 Vue 过渡。可以创建 ~/middleware/disable-vue-transitions.global.ts,内容如下:

export default defineNuxtRouteMiddleware((to) => {
  if (import.meta.server || !document.startViewTransition) {
    return
  }

  // 禁用内置 Vue 过渡
  to.meta.pageTransition = false
  to.meta.layoutTransition = false
})

已知问题

  • 如果你在页面 setup 函数中做数据获取,目前可能需要重新考虑是否使用这个功能。(按照设计,View Transitions 在执行期间会完全冻结 DOM 更新。)我们正在研究将 View Transition 限制在 <Suspense> resolve 之前的最后时刻,但在此期间,如果你的场景涉及数据获取,请仔细考虑是否采用这个功能。