如何实现与示例GIF一致的带动画UI卡片组件?
问题描述
我尝试创建了带背景渐变和动画的卡片组件,但效果与提供的GIF相差甚远。请问如何实现平滑动画、精准渐变,以及如图所示的卡片弹出效果?是否需要使用动画库?能否提供实现指导或解决方案?
[注意:请点击GIF查看,它可能未在问题区域正常渲染]
组件代码
ScrollableCardList 组件
import { useCallback, useEffect, useRef, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Card from "../Card"; import demoCardData from "../../constants/cardData"; export const ScrollableCardList = ({ cardData: propCardData, }: { cardData?: Array<any>; }) => { const data = propCardData || demoCardData; const containerRef = useRef<HTMLDivElement>(null); const [activeIndex, setActiveIndex] = useState(0); const handleScroll = useCallback(() => { if (!containerRef.current) return; const scrollTop = containerRef.current.scrollTop; const scrollHeight = containerRef.current.scrollHeight; const clientHeight = containerRef.current.clientHeight; const scrollRatio = scrollTop / (scrollHeight - clientHeight); const newActiveIndex = Math.floor(scrollRatio * data.length); setActiveIndex(newActiveIndex); }, [data]); useEffect(() => { if (!containerRef.current) return; containerRef.current.addEventListener("scroll", handleScroll); return () => { if (!containerRef.current) return; containerRef.current.removeEventListener("scroll", handleScroll); }; }, [handleScroll]); return ( <div ref={containerRef} className="flex flex-col items-center h-screen overflow-auto" style={{ scrollBehavior: "smooth" }} > {data.map((card, index) => ( <Card key={uuidv4()} imageUrl={card.imageUrl} avatarImageUrl={card.avatarImageUrl} titleText={card.titleText} subTitleText={card.subTitleText} isActive={index === activeIndex} /> ))} </div> ); };
Card 组件
import { Circular, Rectangular, SubTitle, Title } from "./placeholders"; type CardProps = { isActive?: boolean; cardContainerStyles?: string; rectangleContainerStyles?: string; circleContainerStyles?: string; imageUrl?: string; avatarImageUrl?: string; titleText?: string; subTitleText?: string; }; const Card = ({ isActive, cardContainerStyles, rectangleContainerStyles, circleContainerStyles, imageUrl, avatarImageUrl, titleText, subTitleText, }: CardProps) => { const activeClass = isActive ? "scale-110" : ""; // Ensure transition-transform is part of the base class string const cardBaseStyles = "transition-transform duration-300 ease-in-out"; const cardDynamicStyles = cardContainerStyles || "w-80 min-h-80 rounded-2xl shadow-4xl p-4 my-4 bg-gradient-to-br from-rose-500 to-rose-400"; const cardStyles = `${cardBaseStyles} ${activeClass} ${cardDynamicStyles}`; const rectangleStyles = rectangleContainerStyles || "bg-gradient-to-br from-rose-400 to-[#db7483] w-[100%] h-36 rounded-2xl shadow-2xl"; const circleStyles = circleContainerStyles || "w-12 h-12 rounded-full bg-gradient-to-br from-rose-400 to-rose-300 shadow-md"; return ( <div className={cardStyles}> <Rectangular rectangleStyles={rectangleStyles} imageUrl={imageUrl} /> <div className="flex items-center mt-5"> <Circular circleStyles={circleStyles} avatarImageUrl={avatarImageUrl} /> <div className="flex-grow ml-4"> <Title titleText={titleText} /> <SubTitle subTitleText={subTitleText} /> </div> </div> </div> ); }; export default Card;
解决方案
一、平滑动画与卡片弹出效果优化
- 替换生硬的激活判断逻辑
当前通过滚动比例取整切换激活卡片的方式会导致动画突兀,改成基于卡片视口位置的实时计算:
- 在
ScrollableCardList的滚动监听中,用getBoundingClientRect()获取每个卡片的位置,对比视口中心坐标,找到最接近中心的卡片作为激活项,实现平滑过渡。
- 优化动画过渡细节
- 给卡片添加多属性过渡,除了缩放,加上透明度、阴影变化,让层级感更自然:
const cardBaseStyles = "transition-all duration-400 cubic-bezier(0.34, 1.56, 0.64, 1)"; - 调整激活/非激活状态的样式,弱化非激活卡片,强化激活卡片的视觉突出:
const activeClass = isActive ? "scale-108 shadow-xl opacity-100 z-10" : "scale-95 shadow-md opacity-80";
二、精准渐变实现
- 统一渐变色值体系
当前卡片和内部元素的渐变存在色差,统一色值确保视觉连贯,比如:
- 卡片背景:
bg-gradient-to-br from-rose-500 to-fuchsia-600 - 内部矩形:
bg-gradient-to-br from-fuchsia-600 to-rose-400
- 指定精确渐变角度
如果默认方向不符合需求,用具体角度定义渐变,比如bg-gradient-[135deg] from-rose-500 to-fuchsia-600,让渐变方向完全匹配目标效果。
三、是否需要动画库?
不需要额外动画库,原生CSS过渡+React状态足够实现当前需求。如果后续要做更复杂的时序动画,可以考虑framer-motion,但当前场景没必要。
四、关键代码调整示例
修改ScrollableCardList的滚动监听逻辑
const handleScroll = useCallback(() => { if (!containerRef.current) return; const viewportCenter = window.innerHeight / 2; let closestIndex = 0; let minDistance = Infinity; containerRef.current.querySelectorAll('.card').forEach((card, index) => { const rect = card.getBoundingClientRect(); const cardCenter = rect.top + rect.height / 2; const distance = Math.abs(cardCenter - viewportCenter); if (distance < minDistance) { minDistance = distance; closestIndex = index; } }); setActiveIndex(closestIndex); }, [data]);
记得给Card组件根div加上card类名,方便选择器获取。
调整Card组件样式
const cardBaseStyles = "transition-all duration-400 cubic-bezier(0.34, 1.56, 0.64, 1)"; const activeClass = isActive ? "scale-108 shadow-xl opacity-100 z-10" : "scale-95 shadow-md opacity-80"; const cardDynamicStyles = cardContainerStyles || "w-80 min-h-80 rounded-2xl p-4 my-4 bg-gradient-to-br from-rose-500 to-fuchsia-600";
内容的提问来源于stack exchange,提问作者Habi
相关产品推荐
相关产品推荐

