React组件更新CSS变量后Safari未触发动画更新问题
问题:Safari无法同步CSS变量更新到CSS Animations
我在React应用中用CSS Animations实现垂直图片轮播,动画依赖JS通过setProperty设置的CSS变量。轮播容器固定尺寸,包含10个独立动画的幻灯片,SCSS代码如下:
$margin: 25px; $max-images-in-view: 10; $speed: 90; $offset: calc($speed * 1000ms); .slide { animation-iteration-count: infinite; animation-timing-function: linear; animation-duration: #{$speed}s; @for $i from 1 through 10 { &:nth-child(#{$i}) { animation-delay: calc( 1000ms * $speed/$max-images-in-view * (#{$i} + 1) - $offset ); animation-name: slide-animation-#{$i}; top: 100%; @keyframes slide-animation-#{$i} { from { top: 100%; opacity: 1; } to { top: calc( 100% - $max-images-in-view * (var(--image-reel-image-height) + $margin) ); opacity: 1; } } } } }
--image-reel-image-height变量由React组件在加载和屏幕尺寸变化时设置:
const calculateCSSImageHeight = () => { const imgEl = document.getElementsByClassName(Styles.imageContainer).item(0) if (isBrowser) document.documentElement.style.setProperty( '--image-reel-image-height', `${imgEl?.clientHeight ?? 0}px` ) } useEffect(() => { calculateCSSImageHeight() if (typeof window !== 'undefined') window.addEventListener('resize', calculateCSSImageHeight) return () => { window.removeEventListener('resize', calculateCSSImageHeight) } })
该方案在Chrome中正常,但Safari在CSS变量更新后不会同步更新动画——比如默认--image-reel-height设为300px时,Safari始终按300px执行动画,变量更新后无变化。
原因分析
Safari对CSS变量在@keyframes中的动态更新存在兼容性限制:当动画初始化时,Safari会将@keyframes中引用的CSS变量值固化,后续变量变更不会触发动画关键帧的重新计算。而Chrome等浏览器会实时追踪变量变化并更新动画逻辑,这是两者的行为差异。
解决方案
方案1:重置动画强制Safari重新计算
当CSS变量更新后,通过短暂移除并恢复动画属性,触发Safari重新初始化动画,读取最新的变量值。修改React代码如下:
const calculateCSSImageHeight = () => { const imgEl = document.getElementsByClassName(Styles.imageContainer).item(0) if (isBrowser) { document.documentElement.style.setProperty( '--image-reel-image-height', `${imgEl?.clientHeight ?? 0}px` ) // 重置动画 resetSlideshowAnimations() } } const resetSlideshowAnimations = () => { const slides = document.querySelectorAll('.slide') slides.forEach(slide => { const originalAnimName = slide.style.animationName || getComputedStyle(slide).animationName // 临时移除动画 slide.style.animationName = 'none' // 触发重绘(强制浏览器更新DOM状态) void slide.offsetWidth // 恢复原动画 slide.style.animationName = originalAnimName }) } useEffect(() => { calculateCSSImageHeight() window.addEventListener('resize', calculateCSSImageHeight) return () => window.removeEventListener('resize', calculateCSSImageHeight) }, [])
方案2:重构动画逻辑,避免在@keyframes中直接使用变量
将依赖变量的计算移到元素的CSS属性中,让动画仅控制进度而非具体数值,这样变量变化时会直接影响元素属性:
修改SCSS代码:
$margin: 25px; $max-images-in-view: 10; $speed: 90; $offset: calc($speed * 1000ms); .slide { --total-slide-distance: calc($max-images-in-view * (var(--image-reel-image-height) + $margin)); animation-iteration-count: infinite; animation-timing-function: linear; animation-duration: #{$speed}s; animation-name: slide-animation; top: 100%; @for $i from 1 through 10 { &:nth-child(#{$i}) { animation-delay: calc( 1000ms * $speed/$max-images-in-view * (#{$i} + 1) - $offset ); } } } // 通用动画,仅控制位移进度 @keyframes slide-animation { from { transform: translateY(0); opacity: 1; } to { transform: translateY(calc(-1 * var(--total-slide-distance))); opacity: 1; } }
此方案中,--total-slide-distance绑定在.slide元素上,变量更新时会直接影响transform的计算值,Safari能正确识别这种变化,无需额外重置动画。
内容的提问来源于stack exchange,提问作者BlondeSwan
相关产品推荐
相关产品推荐

