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

如何在ReactJS中通过CSS实现轮播图的弧形效果?

问题描述

我需要制作一个如下样式的轮播图:
目标轮播图样式

我已经用ReactJS和CSS实现了类似效果:
当前已实现的轮播图

但还没实现精准的弧形效果,以下是我正在开发的代码:

import React, { useState } from "react";
import Slide from "../CarouselSlide/Slide";
import styles from "./Carousel.module.css";

const Carousel = ({ slides }) => {
  const [slideIndex, setSlideIndex] = useState(0);

  const handlePrevSlide = () => {
    setSlideIndex((prev) => (prev === 0 ? slides.length - 1 : prev - 1));
  };

  const handleNextSlide = () => {
    setSlideIndex((prev) => (prev + 1) % slides.length);
  };

  return (
    <section className={`${styles.slidesWrapper} w-full`}>
      <div className={styles.slides}>
        <button
          className={`${styles.prevSlideBtn} cursor-pointer`}
          onClick={handlePrevSlide}
        >
          <svg
            width="113"
            height="122"
            viewBox="0 0 113 122"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <g filter="url(#filter0_d_429_1959)">
              <circle cx="51.8531" cy="60.8531" r="20.8531" fill="#F49B00" />
              <path
                d="M53.998 56.2952L49.4501 60.8531L53.998 65.411L52.5979 66.8111L46.6399 60.8531L52.5979 54.8951L53.998 56.2952Z"
                fill="white"
              />
            </g>
          </svg>
        </button>

        {[...slides, ...slides, ...slides].map((slide, i) => {
          let offset = slides.length + (slideIndex - i);

          if (typeof slide === "string") {
            return (
              <Slide
                image={slide.image}
                title={slide.title}
                author={slide.author}
                year={slide.year}
                offset={offset}
                key={i}
              />
            );
          } else {
            return (
              <Slide
                image={slide.image}
                title={slide.title}
                author={slide.author}
                year={slide.year}
                offset={offset}
                key={i}
              />
            );
          }
        })}
        <button
          className={`${styles.nextSlideBtn} cursor-pointer`}
          onClick={handleNextSlide}
        >
          <svg
            width="122"
            height="122"
            viewBox="0 0 122 122"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <g filter="url(#filter0_d_429_1955)">
              <circle cx="61.147" cy="60.8531" r="20.8531" fill="#F49B00" />
              <path
                d="M57.0161 65.411L61.5641 60.8531L57.0161 56.2952L58.4162 54.8951L64.3743 60.8531L58.4162 66.8111L57.0161 65.411Z"
                fill="white"
              />
            </g>
          </svg>
        </button>
      </div>
    </section>
  );
};

export default Carousel;
.slidesWrapper {
  @apply min-h-[630px] flex items-center justify-center overflow-hidden;
}

.slidesWrapper *,
.slidesWrapper *::before,
.slidesWrapper *::after {
  @apply relative;
}

.slides {
  @apply grid;
}

.prevSlideBtn,
.nextSlideBtn {
  @apply appearance-none absolute z-[5]  self-center;
}

.prevSlideBtn {
  @apply -left-[37rem];
}

.nextSlideBtn {
  @apply -right-[35rem];
}

Slide.js

import { useRef, useEffect } from "react";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import CardMedia from "@mui/material/CardMedia";
import Card from "@mui/material/Card";
import styles from "./Slide.module.css";

const Slide = ({ image, title, author, year, offset }) => {
  return (
    <div
      className={styles.slide}
      style={{
        "--offset": offset,
        "--dir": offset === 0 ? 0 : offset > 0 ? 0.8 : -0.8,
      }}
    >
      <div className={styles.slideContent}>
        <Card sx={{ boxShadow: 8, borderRadius: 3 }}>
          <CardMedia
            component="img"
            alt="Self-Portrait with Grey Felt Hat"
            image={image}
            sx={{
              height: 350,
              maxWidth: 350,
              objectFit: "fill",
            }}
          />
          <CardContent>
            <Typography
              gutterBottom
              variant="subtitle2"
              fontWeight="bold"
              component="div"
            >
              {title || "Self-Portrait with Grey Felt Hat"}
            </Typography>
            <div className="flex">
              <Typography variant="body2" color="text.secondary">
                {author || "Vincent van Gogh"}
              </Typography>
              <Typography
                component="div"
                variant="body2"
                color="text.secondary"
                className="ml-auto"
              >
                {year || "1887"}
              </Typography>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default Slide;

Slide.module.css

.slideContent {
  @apply transition-transform duration-[0.5s] ease-[ease-in-out] grid content-center rounded-[15px];
  transform-style: preserve-3d;
  transform: perspective(3000px) translateX(calc(100% * var(--offset)))
    rotateY(calc(-45deg * var(--dir)));
}

.slide {
  grid-area: 1 / -1;
}

我试过用:nth-child()伪类调整特定卡片高度,但没生效。


解决方案

要实现目标的弧形轮播效果,核心是让卡片在X轴位移的同时,增加Y轴位置偏移和缩放比例变化,模拟弧形排列的透视感。以下是具体修改步骤:

1. 优化Slide组件的样式变量传递

在Slide.js中,根据offset的绝对值定义层级参数,同时过滤掉超出显示范围的卡片:

const Slide = ({ image, title, author, year, offset }) => {
  // 只保留当前卡片前后各2张的范围,避免无效渲染
  if (Math.abs(offset) > 2) return null;

  return (
    <div
      className={styles.slide}
      style={{
        "--offset": offset,
        "--level": Math.abs(offset),
        "--dir": offset > 0 ? 1 : -1,
      }}
    >
      <div className={styles.slideContent}>
        {/* 原有Card内容保持不变 */}
      </div>
    </div>
  );
};

2. 修改Slide.module.css实现弧形效果

更新slideContent的transform属性,加入Y轴偏移、缩放和层级控制,让弧形更自然:

.slideContent {
  @apply transition-transform duration-[0.5s] ease-[ease-in-out] grid content-center rounded-[15px];
  transform-style: preserve-3d;
  /* 根据层级设置位移、旋转、缩放和Y轴偏移,模拟弧形 */
  transform: perspective(3000px) 
    translateX(calc(120% * var(--offset)))
    translateY(calc(20% * var(--level) * var(--level)))
    rotateY(calc(-30deg * var(--dir) * (2 - var(--level))))
    scale(calc(1 - 0.15 * var(--level)));
  /* 层级越高的卡片层级越低,避免遮挡 */
  z-index: calc(10 - var(--level));
  opacity: calc(1 - 0.1 * var(--level));
}

.slide {
  grid-area: 1 / -1;
}

3. 简化Carousel的卡片渲染逻辑

替换原有的三次重复渲染逻辑,只渲染当前卡片前后的有效范围:

// 替换原map代码,只渲染当前slide前后各2张卡片
{slides.map((_, idx) => {
  const realIndex = (slideIndex + idx - 2 + slides.length) % slides.length;
  const offset = idx - 2;
  const slide = slides[realIndex];
  return (
    <Slide
      image={slide.image}
      title={slide.title}
      author={slide.author}
      year={slide.year}
      offset={offset}
      key={realIndex}
    />
  );
})}

参数调整说明

  • 修改translateY的数值可以改变弧形弯曲程度,数值越大弧度越明显
  • 调整scale的参数可以控制非当前卡片的缩小比例
  • opacity参数可调整侧边卡片的透明度,增强层次感

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 16:34:52