炫酷主题切换

通常情况下,网站只有一套主题,但是随着用户的需求🤓,我们需要为网站添加一个暗黑模式🌗,这个时候就需要切换主题了。而且个人比较喜欢😘黑暗模式,所以这个功能是必须的。在本博客开发过程中,我发现🤩了一个很有意思的 CSS 属性,可以实现炫酷的切换效果,它就是view-transition,但需要搭配 JS 来使用,下面就来看看🧐如何使用吧。

View Transitions

View Transitions API 是一类较新的 web API, 目前, chrome 111+ 已经支持该 API, 利用快照 snapshot 技术为 DOM 更新提供了更加便利的过渡机制。

实现原理

首先通过 JS 进行触发:

document.startViewTransition(() => {
  // do something
})

调用 startViewTransition() 后, 浏览器会记录第一张快照, 然后执行触发 DOM 状态变动的代码.执行完毕后, 浏览器会捕捉第二张快照.

有了这两个两个快照, 浏览器就会在页面的最上层, 构建一个类似下面结构的伪元素树:

:: view-transition
└─ :: view-transition-group(root)
    └─ :: view-transition-image-pair(root)
      ├─ :: view-transition-old(root)
      └─ :: view-transition-new(root)

接下来,我们只需要去控制这个些伪元素树,即可实现过渡动画效果。

实现代码

首先是 CSS 部分:

/** 取消该模式的默认过渡动画 */
::view-transition-old(root),
::view-transition-new(root) {
  mix-blend-mode: normal;
  animation: none;
}

/** 切换主题时调整快照的顺序 */
::view-transition-old(root) {
  z-index: 999;
}
::view-transition-new(root) {
  z-index: 1;
}
.dark::view-transition-old(root) {
  z-index: 1;
}
.dark::view-transition-new(root) {
  z-index: 999;
}

是的 CSS 部分就这么简单,接下来是 JS 部分:

// 使用view-transition进行主题过渡
function toggleTheme(e: MouseEvent) {
  // 不支持view-transition则直接切换主题
  // @ts-expect-error failed to resolve types
  if (!document.startViewTransition) {
    toggleDark()
    return
  }

  const x = e.clientX
  const y = e.clientY
  const radius = Math.hypot(
    Math.max(x, window.innerWidth - x),
    Math.max(y, window.innerHeight - y),
  )

  // @ts-expect-error failed to resolve types
  const trans = document.startViewTransition(() => {
    toggleDark()
    const root = document.documentElement
    root.classList.remove(isDark.value ? 'light' : 'dark')
    root.classList.add(isDark.value ? 'dark' : 'light')
  })

  trans.ready.then(() => {
    const clipPath = [
    `circle(0px at ${x}px ${y}px)`,
    `circle(${radius}px at ${x}px ${y}px)`,
    ]
    document.documentElement.animate(
      {
        clipPath: isDark.value ? clipPath : [...clipPath].reverse(),
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: `::view-transition-${isDark.value ? 'new' : 'old'}(root)`,
      },
    )
  })
}

其中 toggleDark()isDarkvueuse 中用来切换与记录当前主题状态的,如果你不使用 vueuse,你可以自己实现一个,例如,isDark 可以使用以下代码实现:

const isDark = root.classList.contains('dark')

调用 startViewTransition() 后, 会返回一个 ViewTransition 对象,该对象提供对过渡达到不同状态(例如,准备运行动画或动画完成)或完全跳过过渡做出反应的功能。其中ready属性返回一个Promise,创建伪元素树并且过渡动画即将开始时,该 Promise 将被解析。

在本次实现中,我使用了 clip-path 属性来实现动画效果(这才是炫酷本酷😎,通过点击的位置去圆形过渡),当然你也可以使用其他的属性,比如 transformopacity 等等,只要你能实现你想要的效果就行。

还需注意

  • 再进行过渡时,页面将会失去交互能力,直到过渡结束。
  • TS类型不支持,需要使用 @ts-expect-error 来忽略类型检查。
  • CSS中如果使用了stylelint,需要忽略 ::view-transition 伪元素。
  • 该 API 目前还处于实验阶段,可能会有较大的改动,不建议在生产环境中使用(我用了🤨)。

尝试一下

点击下面的按钮(或点击顶部的切换按钮),切换一下主题吧!🤗 如果在切换时没有圆圈效果,那就是你的浏览器不支持该 API,可以使用 can i use 查看浏览器兼容性。

参考资料

1、JS startViewTransition() Method
2、CSS View Transitions Module