You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

代码说明

  1. 关键帧内的延迟实现:通过SCSS循环@for,为每个图片的延迟值计算出对应的动画进度百分比,在该百分比之前保持元素初始位置,之后开始滚动。
  2. 同步重置逻辑:将动画的重置动作放在100%进度,且从99%到100%瞬间完成,这样所有元素会同时回到初始位置,不会有延迟。
  3. 组件代码无需修改:原来的--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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 15:07:03