Please wait...

小波Note

四川 · 成都市小雨7 ℃
English

Black and white switch animation

This article is optional language
阿坝藏族羌族自治州Sun, February 2, 2025 at 7 PM4.62k273Estimated reading time 9 min
QR code
FavoriteCtrl + D

Recently, I wanted to implement a black-and-white toggle animation. I saw the effect on antfu's blog and thought it was pretty good. I looked at the source code, and since I am using nuxt3, I just used it directly.

antfu

Specific code location: https://github.com/antfu/antfu.me/blob/main/src/logics/index.ts

Complete Code

DarkToggle.vue
        <script setup lang="ts">
const mode = useColorMode()
const isDark = computed<boolean>({
  get() {
    return mode.value === 'dark'
  },
  set() {
    mode.preference = isDark.value ? 'light' : 'dark'
  },
})

/**
 * Credit to [@hooray](https://github.com/hooray)
 * @see https://github.com/vuejs/vitepress/pull/2347
 */
function toggle(event?: MouseEvent) {
  const isAppearanceTransition = typeof document !== 'undefined'
  // @ts-expect-error document.startViewTransition can be undefined
    && document.startViewTransition
    && !window.matchMedia('(prefers-reduced-motion: reduce)').matches

  if (!isAppearanceTransition || !event) {
    isDark.value = !isDark.value
    return
  }

  const x = event.clientX
  const y = event.clientY
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y),
  )
  const transition = document.startViewTransition(async () => {
    isDark.value = !isDark.value
    await nextTick()
  })

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

const context = {
  mode,
  isDark,
  toggle,
}
</script>

<template>
  <ClientOnly placeholder-tag="span">
    <slot v-bind="context" />
  </ClientOnly>
</template>

<style>
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
::view-transition-old(root) {
  z-index: 1;
}
::view-transition-new(root) {
  z-index: 9999;
}
.dark::view-transition-old(root) {
  z-index: 9999;
}
.dark::view-transition-new(root) {
  z-index: 1;
}
</style>

    
Collapse

Code Explanation

mode uses the useColorMode hook to get the current color mode.

isDark is a computed property that determines whether the current mode is dark mode and provides the functionality to toggle the mode.

The toggle function is used to switch between dark mode and light mode.

isAppearanceTransition checks if view transition animations are supported and if the user has not opted to reduce motion effects. If view transition animations are not supported or no event is passed, it directly toggles the mode. If view transition animations are supported, it calculates the click position and the end radius of the transition animation, and starts the view transition animation, toggling the mode during the animation.

Math.hypot calculates the hypotenuse of a right triangle, ensuring the animation covers the entire viewport.

innerWidth is a read-only property that returns the width of the window's document display area in pixels, including the width of the scrollbar (if any). When calculating the animation spread radius, we use innerWidth to determine the farthest distance from the trigger point to the edge of the viewport.

transition.ready.then defines the transition animation effect, using clipPath to achieve a circular expansion animation effect.

Switching to dark mode: clipPath.reverse() expands from large to small.

Switching to light mode: clipPath expands from small to large.

Usage

vue
        <template>
    ...others
    <DarkToggle v-if="isDesktop">
        <template #default="{ toggle }">
        <div cursor="pointer" @click="toggle">
            <Icon size="20" name="material-symbols:light-mode-outline-rounded" dark="hidden" />
            <Icon size="20" name="material-symbols:nightlight-outline-rounded" light="hidden" />
        </div>
        </template>
    </DarkToggle>
    ...others
</template>

    

Effect

it-fb

Astral