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

React实现带交互动画的类轮播卡片组件CSS方案

React 卡片轮播式提交动画实现方案

需求对齐

先明确要实现的效果,避免偏差:

  • 初始状态:页面中心仅展示1张可输入卡片
  • 第一次在卡片内输入内容按回车:已填写的卡片平滑向左移动同时缩小,中心位置生成新的空白待填卡片
  • 已有左侧缩小历史卡+中心待填卡时再次按回车提交:三个动画同步触发
    • 最左侧的旧卡片平滑移动+缩小+淡出隐藏
    • 当前中心已填卡片左移缩小为历史卡尺寸
    • 新空白卡片从右侧入场,初始宽高为0,向中心移动过程中逐渐放大到正常尺寸,最终停在中心
  • 固定规则:中心永远是待填写的新卡片,左侧最多保留1张上一条提交的缩小历史卡,所有过渡效果平滑无跳变

核心实现逻辑

不需要引入第三方动画库,纯CSS过渡+React状态控制即可实现,核心原则是:

  1. 所有卡片用绝对定位堆叠在容器内,位置、尺寸、透明度变化全部交给CSS transition 处理,JS只负责维护卡片的状态类名,不做逐帧计算
  2. 给每个位置的卡片定义固定的CSS状态:中心态、左侧历史态、隐藏态、入场初始态,状态切换时浏览器自动补间动画
  3. 卡片列表最多同时保留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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 21:00:54