CSS动画重置与animation-delay问题求助(Next.js+SCSS)
问题描述
我在落地页实现图片垂直滚动动画:一组图片滚动到指定位置后重置,基础逻辑正常。但给每张图片添加动态animation-delay后,首次动画循环正常,重置时却因延迟属性导致重置动作被延迟。需求是:动画运行阶段保留延迟效果,周期结束后所有元素立即同步重置,之后重新遵循延迟规则。使用Next.js + SCSS开发,相关代码如下:
原Carousel.js代码
import Image from 'next/image'; import styles from '@/styles/components/LandingCarousel.module.scss'; export default function LandingCarousel() { const images = [ { src: '/NetworkingSkeleton.png', delay: 6 }, { src: '/NetworkingSkeleton.png', delay: 7 }, { src: '/NetworkingPreview.png', delay: 8 }, { src: '/NetworkingSkeleton.png', delay: 9 }, { src: '/NetworkingSkeleton.png', delay: 10 }, { src: '/NetworkingSkeleton.png', delay: 11 }, { src: '/NetworkingSkeleton.png', delay: 12 }, { src: '/NetworkingPreview.png', delay: 13 }, { src: '/NetworkingSkeleton.png', delay: 14 }, { src: '/NetworkingSkeleton.png', delay: 15 }, ]; const listingImages = [ { src: '/ListingSkeleton.png', delay: 6 }, { src: '/ListingSkeleton.png', delay: 7 }, { src: '/ListingPreview.png', delay: 8 }, { src: '/ListingSkeleton.png', delay: 9 }, { src: '/ListingSkeleton.png', delay: 10 }, { src: '/ListingSkeleton.png', delay: 11 }, { src: '/ListingPreview.png', delay: 12 }, { src: '/ListingSkeleton.png', delay: 13 }, ]; return ( <div className={styles.section}> <div className={styles.container}> {images.map((image, index) => ( <img key={index} src={image.src} className={styles.animated_image} style={{ '--delay': image.delay }} /> ))} </div> <div className={styles.containerRight}> {listingImages.map((image, index) => ( <img key={index} src={image.src} className={styles.animated_imageRight} style={{ '--delay': image.delay }} /> ))} </div> </div> ); }
原LandingCarousel.module.scss代码
.section { display: flex; height: 87vh; overflow: hidden; align-items: center; .container { display: flex; flex-direction: column; height: calc(400px * 5); .animated_image { display: flex; position: relative; max-height: 400px; object-fit: cover; animation: scroll 8s ease-in-out infinite; -webkit-animation: scroll 8s ease-in-out infinite; // animation-delay: calc(var(--delay) * 0.05s); // -webkit-animation-delay: calc(var(--delay) * 0.05s); } } .containerRight { display: flex; flex-direction: column; height: calc(380px * 4); .animated_imageRight { display: flex; max-height: 380px; object-fit: cover; animation: scrollRight 8s ease-in-out infinite; -webkit-animation: scrollRight 8s ease-in-out infinite; // animation-delay: calc(var(--delay) * 0.05s); // -webkit-animation-delay: calc(var(--delay) * 0.05s); } } } @keyframes scroll { 0% { -webkit-transform: translateY(0); transform: translateY(0); } 50% { -webkit-transform: translateY(-400px * 5); transform: translateY(-400px * 5); } 100% { -webkit-transform: translateY(-400px * 5); transform: translateY(-400px * 5); } } @keyframes scrollRight { 0% { -webkit-transform: translateY(0); transform: translateY(0); } 50% { -webkit-transform: translateY(-380px * 4); transform: translateY(-380px * 4); } 100% { -webkit-transform: translateY(-380px * 4); transform: translateY(-380px * 4); } }
解决方案
核心思路是放弃使用animation-delay,转而在关键帧内部实现延迟逻辑,这样延迟只作用于动画启动阶段,重置阶段所有元素同步回到初始位置。
修改后的SCSS代码
.section { display: flex; height: 87vh; overflow: hidden; align-items: center; .container { display: flex; flex-direction: column; height: calc(400px * 5); .animated_image { display: flex; position: relative; max-height: 400px; object-fit: cover; animation: scroll 8s ease-in-out infinite; -webkit-animation: scroll 8s ease-in-out infinite; // 计算延迟对应的动画时长比例 --delay-ms: calc(var(--delay) * 0.05s); } } .containerRight { display: flex; flex-direction: column; height: calc(380px * 4); .animated_imageRight { display: flex; max-height: 380px; object-fit: cover; animation: scrollRight 8s ease-in-out infinite; -webkit-animation: scrollRight 8s ease-in-out infinite; --delay-ms: calc(var(--delay) * 0.05s); } } } @keyframes scroll { // 初始状态:所有元素在顶部 0% { transform: translateY(0); -webkit-transform: translateY(0); } // 为每个延迟值生成对应的关键帧,保持初始位置直到延迟结束 @for $delay-val from 6 through 15 { $delay-duration: calc(#{$delay-val} * 0.05s); $delay-percent: calc(#{$delay-duration} / 8s * 100%); #{$delay-percent} { transform: translateY(0); -webkit-transform: translateY(0); } } // 延迟结束后,统一在50%进度时滚动到目标位置 50% { transform: translateY(calc(-400px * 5)); -webkit-transform: translateY(calc(-400px * 5)); } // 保持目标位置到99%进度 99% { transform: translateY(calc(-400px * 5)); -webkit-transform: translateY(calc(-400px * 5)); } // 100%进度时瞬间回到初始位置,实现同步重置 100% { transform: translateY(0); -webkit-transform: translateY(0); } } @keyframes scrollRight { 0% { transform: translateY(0); -webkit-transform: translateY(0); } @for $delay-val from 6 through 13 { $delay-duration: calc(#{$delay-val} * 0.05s); $delay-percent: calc(#{$delay-duration} / 8s * 100%); #{$delay-percent} { transform: translateY(0); -webkit-transform: translateY(0); } } 50% { transform: translateY(calc(-380px * 4)); -webkit-transform: translateY(calc(-380px * 4)); } 99% { transform: translateY(calc(-380px * 4)); -webkit-transform: translateY(calc(-380px * 4)); } 100% { transform: translateY(0); -webkit-transform: translateY(0); } }
代码说明
- 关键帧内的延迟实现:通过SCSS循环
@for,为每个图片的延迟值计算出对应的动画进度百分比,在该百分比之前保持元素初始位置,之后开始滚动。 - 同步重置逻辑:将动画的重置动作放在100%进度,且从99%到100%瞬间完成,这样所有元素会同时回到初始位置,不会有延迟。
- 组件代码无需修改:原来的
--delay变量传递逻辑保持不变,依然通过inline style为每个图片设置延迟参数。
备选方案(JS控制)
如果CSS方案无法满足需求,也可以用JavaScript监听动画结束事件,强制同步重置:
// 在Carousel组件中添加useEffect import { useEffect, useRef } from 'react'; export default function LandingCarousel() { const imageRefs = useRef([]); const rightImageRefs = useRef([]); useEffect(() => { const resetAnimation = (refs) => { refs.forEach(ref => { if (ref) { // 移除动画类 ref.classList.remove(styles.animated_image); // 强制重绘 void ref.offsetWidth; // 重新添加动画类 ref.classList.add(styles.animated_image); } }); }; // 为每个图片添加动画结束监听 imageRefs.current.forEach(ref => { ref.addEventListener('animationend', () => resetAnimation(imageRefs.current)); }); rightImageRefs.current.forEach(ref => { ref.addEventListener('animationend', () => resetAnimation(rightImageRefs.current)); }); return () => { // 清理事件监听 imageRefs.current.forEach(ref => { ref.removeEventListener('animationend', () => resetAnimation(imageRefs.current)); }); rightImageRefs.current.forEach(ref => { ref.removeEventListener('animationend', () => resetAnimation(rightImageRefs.current)); }); }; }, []); // 在map渲染时添加ref return ( <div className={styles.section}> <div className={styles.container}> {images.map((image, index) => ( <img key={index} ref={el => imageRefs.current[index] = el} src={image.src} className={styles.animated_image} style={{ '--delay': image.delay }} /> ))} </div> {/* 右侧容器同理 */} <div className={styles.containerRight}> {listingImages.map((image, index) => ( <img key={index} ref={el => rightImageRefs.current[index] = el} src={image.src} className={styles.animated_imageRight} style={{ '--delay': image.delay }} /> ))} </div> </div> ); }
注:JS方案可能存在轻微闪烁,优先推荐纯CSS方案。
内容的提问来源于stack exchange,提问作者Christopher Koziol
相关产品推荐
相关产品推荐

