求助:React实现垂直滚动时的水平堆叠切换动画效果
如何实现垂直滚动触发的水平堆叠切换动画(React)
问题描述
我尝试实现垂直滚动时的水平堆叠切换效果,但一直没成功。目前通过监听滚动位置切换图片和文本内容,但React里加不上过渡动画,不确定是不是实现方式错了。附上当前代码:
原React组件代码
function Section() { const colors = [ <div className={styles.card}> <img src={img1} alt="" /> </div>, <div className={styles.card}> <img src={img2} alt="" /> </div>, <div className={styles.card}> <img src={img3} alt="" /> </div>, <div className={styles.card}> <img src={img4} alt="" /> </div>, ]; const headings = [ "X", "Y", "Z", "W", ]; const contents = [ "X", "Y", "Z", "W", ]; const [scrolling, setScrolling] = useState(colors[0]); const [heading, setHeading] = useState(headings[0]); const [content, setContent] = useState(contents[0]); const containerRef = useRef(null); const handleScroll = () => { if (!containerRef.current) return; const scrollTop = containerRef.current.getBoundingClientRect().top; console.log(scrollTop); if (scrollTop >= -500 && scrollTop < 0) { setScrolling(colors[0]); setHeading(headings[0]); setContent(contents[0]); } else if (scrollTop >= -1000 && scrollTop < -500) { setScrolling(colors[1]); setHeading(headings[1]); setContent(contents[1]); } else if (scrollTop >= -1500 && scrollTop < -1000) { setScrolling(colors[2]); setHeading(headings[2]); setContent(contents[2]); } else if (scrollTop >= -2000 && scrollTop < -1500) { setScrolling(colors[3]); setHeading(headings[3]); setContent(contents[3]); } }; useEffect(() => { const parentElement = containerRef.current; parentElement.addEventListener("scroll", handleScroll); return () => { parentElement.removeEventListener("scroll", handleScroll); }; }, []); useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []); return ( <div className={styles.cardContainer} ref={containerRef}> <div className={styles.wrapper}> <section className={styles.cardParent}> {scrolling} {window.innerWidth > 1100 && ( <React.Fragment> <div className={`${styles.card} ${styles.dummy1}`}> <img src={bg} alt="" /> </div> <div className={`${styles.card} ${styles.dummy2}`}> <img src={bg} alt="" /> </div> </React.Fragment> )} </section> <aside className={styles.sideHeadings}> <section className={styles.sideHeadingsContent}> <h3>{heading}</h3> <p>{content}</p> <Link to={"/signup"} className="btn"> Let's Start </Link> </section> </aside> </div> </div> ); }
原CSS代码
.cardContainer { display: flex; justify-content: space-around; position: relative; margin-top: 10vh; height: 2300px; width: 90vw; overflow: visible; } .wrapper { display: flex; gap: 3rem; justify-content: space-between; align-items: center; position: sticky; top: 10vh; height: 70vh; width: 100%; } .cardParent { height: 100%; width: 90%; } .card { height: 100%; width: 100%; z-index: 999; } .card > img { margin-right: auto; width: fit-content; height: 100% ; object-fit: contain; } .dummy1{ height: 100%; width: fit-content; position: absolute; top: 0; z-index: -9; display: flex; } .dummy1 > img{ height: 90%; width: 100%; translate: 15% 5% 0; object-fit: contain; border-radius: 3rem !important; border-right: 1px solid rgb(149,149,149); } .dummy2 { height: 100%; width: fit-content; position: absolute; top: 0; z-index: -90; display: flex; } .dummy2 > img{ height: 80%; width: 100%; translate: 33% 12% 0; object-fit: contain; border-radius: 2rem !important; border-right: 1px solid rgb(149,149,149); } .sideHeadings { display: flex; align-items: center; height: 100%; width: 100%; } .sideHeadingsContent{ display: flex; flex-direction: column; justify-content: space-between; height:60%; width:100%; } .sideHeadingsContent > h3 { font-size: var(--h2-large); font-family: var(--font-inter); font-weight: 400; color: rgb(201,201,201); } .sideHeadingsContent >p{ font-family: var(--font-poppins); color: rgb(149,149,149); font-size: var(--para-large); } .sideHeadingsContent > a{ height: 50px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 500; background: linear-gradient(90deg,rgba(221,255,0,.5)60%,rgb(221,255,0) 100%); /* transition: all .6s ease !important; */ } .sideHeadingsContent > a:hover{ background: black !important; } /* Tablet */ @media (width<= 1100px) { .cardContainer{ margin-bottom: 30vh; } .wrapper { flex-direction: column; } .sideHeadings{ width: 90vw; } .sideHeadingsContent{ justify-content: center; align-items: center; text-align: center; gap: 2rem; width: 90vw; } .sideHeadingsContent > h3{ font-size: var(--h3-large); } .wrapper img{ width: 90vw; height: 40vh; } .sideHeadingsContent > a{ margin: 0; font-size: 16px; } } @media (width<=768px) { .sideHeadingsContent > h1{ font-size: var(--h1-heading-mobile); } .sideHeadingsContent > h3{ font-size: var(--h3-heading-mobile); } .wrapper img{ width: 90vw; height: 40vh; } }
问题分析
- 直接替换JSX元素会导致React卸载旧元素、渲染新元素,无法触发CSS过渡——过渡需要元素状态变化而非替换
- 同时监听容器和window滚动会导致逻辑冲突,实际只需监听window滚动(容器是静态高度,靠window滚动触发位置变化)
- 未给切换元素添加过渡状态的CSS规则,没有动画触发条件
解决方案与修改后代码
1. 重构React组件逻辑
- 用索引控制内容显示,保留所有元素在DOM中,通过CSS类切换状态
- 基于滚动位置计算当前激活索引,实现平滑切换
- 清理冗余的滚动监听
修改后的React组件代码
import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import img1 from './path/to/img1.jpg'; import img2 from './path/to/img2.jpg'; import img3 from './path/to/img3.jpg'; import img4 from './path/to/img4.jpg'; import bg from './path/to/bg.jpg'; import styles from './Section.module.css'; function Section() { const items = [ { img: img1, heading: "X", content: "X内容描述" }, { img: img2, heading: "Y", content: "Y内容描述" }, { img: img3, heading: "Z", content: "Z内容描述" }, { img: img4, heading: "W", content: "W内容描述" }, ]; const [activeIndex, setActiveIndex] = useState(0); const containerRef = useRef(null); const itemHeight = 500; // 每个切换区间的高度 const handleScroll = () => { if (!containerRef.current) return; const containerTop = containerRef.current.getBoundingClientRect().top; // 计算当前滚动到的区间索引 const index = Math.floor(Math.abs(containerTop) / itemHeight); // 限制索引范围 const clampedIndex = Math.max(0, Math.min(index, items.length - 1)); if (clampedIndex !== activeIndex) { setActiveIndex(clampedIndex); } }; useEffect(() => { window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); return ( <div className={styles.cardContainer} ref={containerRef}> <div className={styles.wrapper}> <section className={styles.cardParent}> {/* 所有卡片保留在DOM中,通过active类控制显示 */} {items.map((item, index) => ( <div key={index} className={`${styles.card} ${index === activeIndex ? styles.active : ''}`} > <img src={item.img} alt={item.heading} /> </div> ))} {window.innerWidth > 1100 && ( <> <div className={`${styles.card} ${styles.dummy1}`}> <img src={bg} alt="背景占位" /> </div> <div className={`${styles.card} ${styles.dummy2}`}> <img src={bg} alt="背景占位" /> </div> </> )} </section> <aside className={styles.sideHeadings}> <section className={styles.sideHeadingsContent}> {/* 文本内容同样通过active类切换动画 */} {items.map((item, index) => ( <div key={index} className={`${styles.textContent} ${index === activeIndex ? styles.active : ''}`} > <h3>{item.heading}</h3> <p>{item.content}</p> </div> ))} <Link to={"/signup"} className="btn"> Let's Start </Link> </section> </aside> </div> </div> ); } export default Section;
2. 添加CSS过渡动画
- 给卡片和文本元素添加
transition属性 - 用
opacity和transform实现淡入淡出+位移效果 - 控制元素层级确保激活内容在最上层
修改后的CSS代码
.cardContainer { display: flex; justify-content: space-around; position: relative; margin-top: 10vh; height: 2300px; width: 90vw; overflow: visible; } .wrapper { display: flex; gap: 3rem; justify-content: space-between; align-items: center; position: sticky; top: 10vh; height: 70vh; width: 100%; } .cardParent { height: 100%; width: 90%; position: relative; /* 卡片绝对定位的容器 */ } .card { position: absolute; /* 所有卡片重叠 */ top: 0; left: 0; height: 100%; width: 100%; opacity: 0; transform: translateX(20px); transition: all 0.5s ease; z-index: 1; } .card.active { opacity: 1; transform: translateX(0); z-index: 999; } .card > img { margin-right: auto; width: fit-content; height: 100% ; object-fit: contain; } .dummy1, .dummy2 { opacity: 1; transform: none; transition: none; /* 占位卡片不需要动画 */ } .dummy1{ height: 100%; width: fit-content; position: absolute; top: 0; z-index: -9; display: flex; } .dummy1 > img{ height: 90%; width: 100%; translate: 15% 5% 0; object-fit: contain; border-radius: 3rem !important; border-right: 1px solid rgb(149,149,149); } .dummy2 { height: 100%; width: fit-content; position: absolute; top: 0; z-index: -90; display: flex; } .dummy2 > img{ height: 80%; width: 100%; translate: 33% 12% 0; object-fit: contain; border-radius: 2rem !important; border-right: 1px solid rgb(149,149,149); } .sideHeadings { display: flex; align-items: center; height: 100%; width: 100%; } .sideHeadingsContent{ display: flex; flex-direction: column; justify-content: space-between; height:60%; width:100%; position: relative; /* 文本绝对定位的容器 */ } .textContent { position: absolute; top: 0; left: 0; width: 100%; opacity: 0; transform: translateY(10px); transition: all 0.5s ease; } .textContent.active { opacity: 1; transform: translateY(0); } .sideHeadingsContent > h3 { font-size: var(--h2-large); font-family: var(--font-inter); font-weight: 400; color: rgb(201,201,201); } .sideHeadingsContent > p{ font-family: var(--font-poppins); color: rgb(149,149,149); font-size: var(--para-large); } .sideHeadingsContent > a{ height: 50px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 500; background: linear-gradient(90deg,rgba(221,255,0,.5)60%,rgb(221,255,0) 100%); transition: all .6s ease !important; position: relative; z-index: 1
相关产品推荐
相关产品推荐

