






























这套方案解决的是“主题切换僵硬”的常见问题,让用户点击主题色后看到更丝滑、更高级的视觉反馈:
核心是“三层组合”:
Theme Vars:主题仍然用 CSS 变量控制,保持可维护性。View Transition:在切换瞬间做全局圆形揭幕,形成“高级感”。Fallback:老浏览器走轻量扫光动画,保证兼容。参考项目文件:sidebar-theme.js
const SIDEBAR_THEME_TRANSITION_DURATION = 720
function getThemeTransitionOrigin(options = {}) {
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
const event = options.event
if (event && typeof event.clientX === 'number' && typeof event.clientY === 'number') {
return { x: event.clientX, y: event.clientY, viewportWidth, viewportHeight }
}
const sourceElement = options.sourceElement
if (sourceElement && sourceElement.getBoundingClientRect) {
const rect = sourceElement.getBoundingClientRect()
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
viewportWidth,
viewportHeight
}
}
const sidebar = document.querySelector('.sidebar-container')
if (sidebar && sidebar.getBoundingClientRect) {
const rect = sidebar.getBoundingClientRect()
return {
x: rect.left + rect.width,
y: rect.top + 72,
viewportWidth,
viewportHeight
}
}
return { x: viewportWidth / 2, y: viewportHeight / 2, viewportWidth, viewportHeight }
}
function setThemeTransitionVars(origin) {
const root = document.documentElement
const radius = Math.hypot(
Math.max(origin.x, origin.viewportWidth - origin.x),
Math.max(origin.y, origin.viewportHeight - origin.y)
)
root.style.setProperty('--sidebar-theme-transition-x', `${origin.x}px`)
root.style.setProperty('--sidebar-theme-transition-y', `${origin.y}px`)
root.style.setProperty('--sidebar-theme-transition-radius', `${Math.ceil(radius)}px`)
}
function cleanupThemeTransition() {
const root = document.documentElement
root.classList.remove('sidebar-theme-view-transition')
window.clearTimeout(cleanupThemeTransition.timer)
cleanupThemeTransition.timer = null
}
export function applySidebarThemeWithTransition(config, options = {}) {
const normalized = normalizeSidebarThemeConfig(config)
if (typeof document === 'undefined' || typeof window === 'undefined') {
return applySidebarTheme(normalized)
}
const prefersReducedMotion = window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (prefersReducedMotion || !document.startViewTransition) {
document.body.classList.add('sidebar-theme-is-switching')
const applied = applySidebarTheme(normalized)
window.setTimeout(() => {
document.body.classList.remove('sidebar-theme-is-switching')
}, 360)
return applied
}
cleanupThemeTransition()
setThemeTransitionVars(getThemeTransitionOrigin(options))
document.documentElement.classList.add('sidebar-theme-view-transition')
let transition
try {
transition = document.startViewTransition(() => {
applySidebarTheme(normalized)
})
} catch (e) {
cleanupThemeTransition()
return applySidebarTheme(normalized)
}
if (transition && transition.finished) {
transition.finished.then(cleanupThemeTransition).catch(cleanupThemeTransition)
} else {
cleanupThemeTransition.timer = window.setTimeout(cleanupThemeTransition, SIDEBAR_THEME_TRANSITION_DURATION)
}
return normalized
}
参考项目文件:sidebar.scss
html.sidebar-theme-view-transition::view-transition-old(root),
html.sidebar-theme-view-transition::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
html.sidebar-theme-view-transition::view-transition-group(root) {
animation-duration: 0.72s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
html.sidebar-theme-view-transition::view-transition-new(root) {
animation: sidebar-theme-reveal 0.72s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes sidebar-theme-reveal {
from {
clip-path: circle(0 at var(--sidebar-theme-transition-x, 0px) var(--sidebar-theme-transition-y, 0px));
}
to {
clip-path: circle(var(--sidebar-theme-transition-radius, 140vmax) at var(--sidebar-theme-transition-x, 0px) var(--sidebar-theme-transition-y, 0px));
}
}
@keyframes sidebar-theme-soft-flash {
0% { opacity: 0; transform: scaleX(0.92); }
38% { opacity: 1; }
100% { opacity: 0; transform: scaleX(1.04); }
}
body.sidebar-theme-is-switching #app .sidebar-container::after {
animation: sidebar-theme-soft-flash 0.36s cubic-bezier(0.16, 1, 0.3, 1);
}
参考项目文件:index.vue
<div
v-for="item in presetThemes"
:key="item.key"
class="theme-card"
@click="selectPresetTheme(item, $event)"
/>
import { applySidebarThemeWithTransition } from '@/utils/sidebar-theme'
selectPresetTheme(item, event) {
this.applyTheme(
{ key: item.key, color: item.color },
{ animate: true, event }
)
},
selectCustomTheme(event, animate = true) {
this.applyTheme(
{ key: CUSTOM_SIDEBAR_THEME_KEY, color: this.customThemeColor },
{
animate,
event,
sourceElement: this.$refs.customThemePicker && this.$refs.customThemePicker.$el
}
)
},
applyTheme(theme, options = {}) {
this.draftTheme = { key: theme.key, color: normalizeHexColor(theme.color) }
if (options.animate) {
applySidebarThemeWithTransition(this.draftTheme, options)
return
}
applySidebarTheme(this.draftTheme)
}
建议把以下参数抽成常量,方便在不同项目快速调优:
SIDEBAR_THEME_TRANSITION_DURATION:推荐 620~760ms。cubic-bezier(0.16, 1, 0.3, 1):推荐保留,节奏自然。300~420ms。translateX(1px~3px),不宜过大。document.startViewTransition,展示圆形揭幕。sidebar-theme-is-switching 扫光动画。prefers-reduced-motion: reduce 后禁用复杂动画。这三步做完,功能可用性和体验一致性都能保证。
event 或 sourceElement。prefers-reduced-motion 的兜底。src/utils/sidebar-theme.jssrc/styles/sidebar.scsssrc/views/personality-module/index.vue对应改造可直接作为你后续博客中的“完整示例工程结构”章节。
这套结构对技术读者非常友好,既能讲清原理,也能让人一键接入。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。