主题切换时让新配色从点击位置扩散,可以用浏览器的 View Transition API。浏览器先截取前后两个页面,再把新页面的快照裁剪成一个逐渐变大的圆。在 Astro 中,把交互组件用 client:load 加载即可。
算出能盖住整个视口的半径
从圆心取横向和纵向各自的最大距离,再用 Math.hypot 得到最远角落的距离。鼠标用点击点,键盘用按钮中心。
function toggleTheme(event: MouseEvent) {
const root = document.documentElement
const next = root.dataset.theme === 'dark' ? 'light' : 'dark'
const apply = () => { root.dataset.theme = next }
if (!document.startViewTransition ||
matchMedia('(prefers-reduced-motion: reduce)').matches) {
apply()
return
}
const button = (event.currentTarget as HTMLElement).getBoundingClientRect()
const x = event.detail ? event.clientX : button.left + button.width / 2
const y = event.detail ? event.clientY : button.top + button.height / 2
const radius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)
const clip = (r: number) => 'circle(' + r + 'px at ' + x + 'px ' + y + 'px)'
const transition = document.startViewTransition(apply)
transition.ready.then(() => {
root.animate(
{ clipPath: [clip(0), clip(radius)] },
{ duration: 420, easing: 'ease-out', pseudoElement: '::view-transition-new(root)' }
)
}).catch(() => {
// 快照跳过时,主题仍由 apply 更新。
})
}
关掉默认淡入淡出
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-new(root) {
z-index: 1;
}
配色 CSS 要读同一个 data-theme 属性。如果项目用 html.dark,就把 apply 改成增删 class。需要记住选择时,在初始渲染前读取本地设置,避免刷新时先亮再暗。
这个例子只处理当前页面里的主题切换。原笔记中的 ViewTransitions 是 Astro 路由切换的历史写法。需要跨页面导航动画时,再看当前 Astro 的 ClientRouter。