React实现带交互动画的类轮播卡片组件CSS方案
React 卡片轮播式提交动画实现方案
需求对齐
先明确要实现的效果,避免偏差:
- 初始状态:页面中心仅展示1张可输入卡片
- 第一次在卡片内输入内容按回车:已填写的卡片平滑向左移动同时缩小,中心位置生成新的空白待填卡片
- 已有左侧缩小历史卡+中心待填卡时再次按回车提交:三个动画同步触发
- 最左侧的旧卡片平滑移动+缩小+淡出隐藏
- 当前中心已填卡片左移缩小为历史卡尺寸
- 新空白卡片从右侧入场,初始宽高为0,向中心移动过程中逐渐放大到正常尺寸,最终停在中心
- 固定规则:中心永远是待填写的新卡片,左侧最多保留1张上一条提交的缩小历史卡,所有过渡效果平滑无跳变
核心实现逻辑
不需要引入第三方动画库,纯CSS过渡+React状态控制即可实现,核心原则是:
- 所有卡片用绝对定位堆叠在容器内,位置、尺寸、透明度变化全部交给CSS
transition处理,JS只负责维护卡片的状态类名,不做逐帧计算 - 给每个位置的卡片定义固定的CSS状态:中心态、左侧历史态、隐藏态、入场初始态,状态切换时浏览器自动补间动画
- 卡片列表最多同时保留3个DOM节点(待隐藏旧卡、历史卡、当前新卡),退场动画结束后直接移除旧节点,无额外性能开销
完整代码实现
React 组件代码
import { useState, useCallback } from 'react' import './CardCarousel.css' // 动画参数可按需调整 const ANIMATE_CONFIG = { duration: 400, // 动画时长,单位ms } export default function SubmitCard() { const [cards, setCards] = useState([ { id: Date.now(), value: '', entered: true } ]) const handleEnterSubmit = useCallback((e, targetId) => { if (e.key !== 'Enter') return e.preventDefault() // 追加新卡片,触发动画 setCards(prev => { const nextList = [...prev, { id: Date.now(), value: '', entered: false }] // 超过3张时,第一张为待退场卡片,动画结束后移除 if (nextList.length > 3) { setTimeout(() => { setCards(curr => curr.slice(1)) }, ANIMATE_CONFIG.duration) } // 下一帧切换新卡片的入场状态,触发过渡动画 requestAnimationFrame(() => { requestAnimationFrame(() => { setCards(curr => curr.map((card, idx) => idx === curr.length - 1 ? {...card, entered: true} : card )) }) }) return nextList }) }, []) const handleInputChange = useCallback((e, targetId) => { setCards(prev => prev.map(card => card.id === targetId ? {...card, value: e.target.value} : card )) }, []) const getCardClassName = (index, total, entered) => { const baseClass = 'submit-card' if (total === 1) return `${baseClass} card-center` if (total === 2) { return index === 0 ? `${baseClass} card-left` : `${baseClass} card-center` } if (total >= 3) { if (index === 0) return `${baseClass} card-hide` if (index === 1) return `${baseClass} card-left` if (index === 2) return entered ? `${baseClass} card-center` : `${baseClass} card-enter-init` } return baseClass } return ( <div className="card-container"> {cards.map((card, index) => ( <div key={card.id} className={getCardClassName(index, cards.length, card.entered)} > <input type="text" value={card.value} placeholder="输入内容后按回车提交" onKeyDown={(e) => handleEnterSubmit(e, card.id)} onChange={(e) => handleInputChange(e, card.id)} disabled={index !== cards.length - 1} /> </div> ))} </div> ) }
对应CSS样式代码
.card-container { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #f5f5f7; } .submit-card { position: absolute; width: 400px; height: 240px; background: #ffffff; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); display: flex; align-items: center; justify-content: center; padding: 24px; box-sizing: border-box; /* 核心:所有属性变化统一加平滑过渡 */ transition: all 0.4s cubic-bezier(0.25, 0.1, 0.25, 1); } .submit-card input { width: 100%; padding: 14px 16px; border: 1px solid #e5e5ea; border-radius: 8px; font-size: 16px; outline: none; transition: border-color 0.2s; } .submit-card input:focus { border-color: #007aff; } /* 中心待填卡片状态 */ .card-center { transform: translateX(0) scale(1); opacity: 1; z-index: 10; } /* 左侧历史缩小卡片状态 */ .card-left { transform: translateX(-340px) scale(0.7); opacity: 0.7; z-index: 5; } /* 退场隐藏卡片状态 */ .card-hide { transform: translateX(-560px) scale(0.5); opacity: 0; z-index: 1; } /* 新卡片入场初始状态:右侧0尺寸 */ .card-enter-init { transform: translateX(420px) scale(0); opacity: 0; z-index: 10; }
调优说明
- 动画缓动函数默认用了
cubic-bezier(0.25, 0.1, 0.25, 1),手感接近原生系统的过渡效果,如果想要更弹的效果可以换成cubic-bezier(0.34, 1.56, 0.64, 1) - 卡片尺寸、偏移距离直接修改CSS里对应类的
transform数值和卡片宽高即可,不影响核心动画逻辑 - 用
transform和opacity做动画会触发GPU合成层,不会引起重排,动画帧率可以稳定在60fps - 双重
requestAnimationFrame是为了确保浏览器先渲染新卡片的初始入场状态,再切换到中心状态,避免状态合并导致动画不触发
内容的提问来源于stack exchange,提问作者Aliasghar Ahmadpour
相关产品推荐
相关产品推荐

