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

滚动时重叠图片缩放动画实现问题(React+TypeScript)

滚动重叠图片动画问题修复方案

我想要实现滚动时重叠图片的动画效果:向下滚动时图片放大,向上滚动时缩小。已知需要用Intersection Observer API处理滚动逻辑,尝试实现但未成功。技术栈是React+TypeScript,使用内联样式开发,参考了三重叠图片滚动动画示例及Codepen实现。

原实现代码(OverlappingImages.tsx)

import React from 'react';

const styles = {
    container: {
        position: 'relative',
        height: '400px',
        margin: '0 50px',

        div: {
            width: '380px',
            border: '1px solid #000',
            overflow: 'hidden',
            lineHeight: 0,
            transition: 'transform .4s ease-in-out',

            img: {
                width: '100%',
                fontSize: 0,
            },
        },

        img1: {
            left: '5%',
            top: 0,
            position: 'absolute',
            transform: 'rotate(-4deg) translateY(20%)',
            transitionDelay: '0s',
        },

        img2: {
            left: '50%',
            top: 0,
            position: 'absolute',
            transform: 'translate(-50%, 0)',
            transitionDelay: '.1s',
            zIndex: 1,
        },

        img3: {
            right: '5%',
            top: 0,
            position: 'absolute',
            transform: 'rotate(4deg) translateY(20%)',
            transitionDelay: '.2s',
        },

        ' &.active': {
            img1: {
                transform: 'rotate(-6deg) translateY(50%) scale(1.9)',
            },

            img2: {
                transform: 'translate(-50%, -2%) scale(1.2)',
            },

            img3: {
                transform: 'rotate(6deg) translateY(24%) scale(1.2)',
            },
        },
    },

    body: {
        fontFamily: 'sans-serif',
        fontSize: '48px',
        fontWeight: 'bold',
        letterSpacing: '1px',
        margin: 0,
    },

    section: {
        textAlign: 'center',
        padding: '500px 0',

        '&:nth-child(odd)': {
            background: '#eee',
        },
    },
};

function OverlappingImages() {
    const wrapper = document.querySelector('.container');
    const className = 'active';

    const observer = new IntersectionObserver(
        (entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    wrapper.classList.add(className);
                    return;
                }

                wrapper.classList.remove(className);
            });
        },
        {
            threshold: 1,
        }
    );

    observer.observe(wrapper);

    return (
        <>
            <section>
                <p>(scroll down!)</p>
            </section>
            <section>
                <div style={styles.container}>
                    <div style={styles.container.img1}>
                        <img src="https://via.placeholder.com/350x250" alt="img1" />
                    </div>
                    <div style={styles.container.img2}>
                        <img src="https://via.placeholder.com/350x250" alt="img2" />
                    </div>
                    <div style={styles.container.img3}>
                        <img src="https://via.placeholder.com/350x250" alt="img3" />
                    </div>
                </div>
            </section>
            <section>
                <p>(scroll up!)</p>
            </section>
        </>
    );
}

export { OverlappingImages };

问题分析

  1. DOM操作时机错误:组件渲染时直接调用document.querySelector('.container'),此时DOM还未挂载,会返回null,导致后续Observer无法正常工作。
  2. 内联样式不支持CSS嵌套语法:React内联样式是纯对象,不支持&.active这种CSS类嵌套、伪类/伪元素语法,所以active状态下的样式完全不生效。
  3. 未清理Observer:组件卸载时没有停止Observer监听,会造成内存泄漏。
  4. 直接操作DOM类名不符合React范式:应该通过状态管理来控制样式变化,而非直接修改DOM类名。

修正后的代码

import React, { useRef, useEffect, useState } from 'react';

// 基础样式拆分,避免嵌套
const baseContainerStyle: React.CSSProperties = {
    position: 'relative',
    height: '400px',
    margin: '0 50px',
};

const baseImageWrapperStyle: React.CSSProperties = {
    width: '380px',
    border: '1px solid #000',
    overflow: 'hidden',
    lineHeight: 0,
    transition: 'transform 0.4s ease-in-out',
};

const baseImgStyle: React.CSSProperties = {
    width: '100%',
    fontSize: 0,
};

// 初始状态样式
const img1InitialStyle: React.CSSProperties = {
    ...baseImageWrapperStyle,
    left: '5%',
    top: 0,
    position: 'absolute',
    transform: 'rotate(-4deg) translateY(20%)',
    transitionDelay: '0s',
};

const img2InitialStyle: React.CSSProperties = {
    ...baseImageWrapperStyle,
    left: '50%',
    top: 0,
    position: 'absolute',
    transform: 'translate(-50%, 0)',
    transitionDelay: '0.1s',
    zIndex: 1,
};

const img3InitialStyle: React.CSSProperties = {
    ...baseImageWrapperStyle,
    right: '5%',
    top: 0,
    position: 'absolute',
    transform: 'rotate(4deg) translateY(20%)',
    transitionDelay: '0.2s',
};

// 激活状态样式
const img1ActiveStyle: React.CSSProperties = {
    ...img1InitialStyle,
    transform: 'rotate(-6deg) translateY(50%) scale(1.9)',
};

const img2ActiveStyle: React.CSSProperties = {
    ...img2InitialStyle,
    transform: 'translate(-50%, -2%) scale(1.2)',
};

const img3ActiveStyle: React.CSSProperties = {
    ...img3InitialStyle,
    transform: 'rotate(6deg) translateY(24%) scale(1.2)',
};

const sectionStyle: React.CSSProperties = {
    textAlign: 'center',
    padding: '500px 0',
};

const oddSectionStyle: React.CSSProperties = {
    ...sectionStyle,
    background: '#eee',
};

function OverlappingImages() {
    const containerRef = useRef<HTMLDivElement>(null);
    const [isActive, setIsActive] = useState(false);

    useEffect(() => {
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach(entry => {
                    // 根据交叉状态更新active状态
                    setIsActive(entry.isIntersecting);
                });
            },
            {
                threshold: 1,
            }
        );

        const currentContainer = containerRef.current;
        if (currentContainer) {
            observer.observe(currentContainer);
        }

        // 组件卸载时清理Observer
        return () => {
            if (currentContainer) {
                observer.unobserve(currentContainer);
            }
        };
    }, []);

    return (
        <>
            <section style={oddSectionStyle}>
                <p>(向下滚动!)</p>
            </section>
            <section style={sectionStyle}>
                <div ref={containerRef} style={baseContainerStyle}>
                    <div style={isActive ? img1ActiveStyle : img1InitialStyle}>
                        <img style={baseImgStyle} src="https://via.placeholder.com/350x250" alt="img1" />
                    </div>
                    <div style={isActive ? img2ActiveStyle : img2InitialStyle}>
                        <img style={baseImgStyle} src="https://via.placeholder.com/350x250" alt="img2" />
                    </div>
                    <div style={isActive ? img3ActiveStyle : img3InitialStyle}>
                        <img style={baseImgStyle} src="https://via.placeholder.com/350x250" alt="img3" />
                    </div>
                </div>
            </section>
            <section style={oddSectionStyle}>
                <p>(向上滚动!)</p>
            </section>
        </>
    );
}

export { OverlappingImages };

关键修改说明

  • 使用useRef获取DOM元素:确保组件挂载后能正确获取容器元素,避免null错误。
  • 拆分样式对象:将嵌套的CSS样式拆分为独立的React内联样式对象,通过isActive状态切换不同样式。
  • useEffect管理Observer生命周期:在组件挂载时初始化Observer,卸载时停止监听,避免内存泄漏。
  • 状态驱动样式变化:通过useState维护isActive状态,根据状态切换图片容器的样式,符合React数据驱动视图的范式。

内容的提问来源于stack exchange,提问作者user18943198

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 00:18:21