如何让我的轮播切换更平滑?

前端开发 2026-07-12

我在做一个轮播,展示如下:

  • 居中且处于“活动中”状态的卡片
  • 左侧和右侧的“窥视”相邻卡片(裁剪/遮罩)
  • 上一页/下一页按钮+ 指示点

功能上它能够正常工作,但动画没有像本页的“Thousands of Businesses Transformed”轮播那样顺滑。我该如何模仿这种行为?
https://www.ramseysolutions.com/business/entreleadership

现在我的JS会在每次导航时替换 innerHTML 在插槽中的内容。这会造成跳变/抖动,我无法在旧卡和新卡之间实现平滑的滑动和跨淡入淡出。还有时在过渡期间若存在两个 .ts-slot-inner 元素,会出现一个短暂的“底下多出一张卡”的效果。

function initCarousel(root) {
  const articles = Array.from(root.querySelectorAll(".slides > article"));
  const prevSlot = root.querySelector('[data-slot="prev"]');
  const currSlot = root.querySelector('[data-slot="current"]');
  const nextSlot = root.querySelector('[data-slot="next"]');
  const prevBtn = root.querySelector("[data-prev]");
  const nextBtn = root.querySelector("[data-next]");
  const dotsWrap = root.querySelector("[data-dots]");

  const slides = articles.map(a => ({
    title: a.dataset.title || "Slide",
    html: a.innerHTML
  }));

  const mod = (n, m) => ((n % m) + m) % m;

  const wrapSlot = (html) => `<div class="slot-inner">${html}</div>`;
  const wrapPeek = (html) =>
    `<div class="slot-inner"><div class="peek-inner">${html}</div></div>`;

  let index = 0;

  function renderDots() {
    dotsWrap.innerHTML = slides.map((s, i) => `
      <button type="button" data-dot="${i}" aria-selected="${i === index}">
        <span class="sr-only">${s.title}</span>
      </button>
    `).join("");

    dotsWrap.querySelectorAll("[data-dot]").forEach(btn => {
      btn.addEventListener("click", () => {
        index = Number(btn.dataset.dot);
        update();
      });
    });
  }

  function update() {
    const prevIndex = mod(index - 1, slides.length);
    const nextIndex = mod(index + 1, slides.length);

    prevSlot.innerHTML = wrapPeek(slides[prevIndex].html);
    currSlot.innerHTML = wrapSlot(slides[index].html);
    nextSlot.innerHTML = wrapPeek(slides[nextIndex].html);

    renderDots();
  }

  prevBtn.addEventListener("click", () => {
    index = mod(index - 1, slides.length);
    update();
  });

  nextBtn.addEventListener("click", () => {
    index = mod(index + 1, slides.length);
    update();
  });

  update();
}

document.querySelectorAll(".carousel").forEach(initCarousel);
.carousel-row {
  display: grid;
  grid-template-columns: 2fr 6fr 2fr;
  gap: 16px;
  align-items: stretch;
}

/* slots clip their contents */
.peek, .center {
  position: relative;
  overflow: hidden;
  min-height: 1px;
}

/* if multiple inner wrappers exist, they should overlap */
.peek .slot-inner,
.center .slot-inner {
  position: absolute;
  inset: 0;
  width: 100%;
}

/* keep first one in-flow to preserve height */
.peek > .slot-inner:first-child,
.center > .slot-inner:first-child {
  position: relative;
}

/* side peek masks */
.peek-inner { width: 300%; }
.peek-left  .peek-inner { transform: translateX(-66.6667%); }
.peek-right .peek-inner { transform: translateX(0); }

.card {
  background: white;
  padding: 16px;
  border-radius: 16px;
  box-shadow: 0 0 8px rgba(0,0,0,.15);
  text-align: center;
}

.card-green  { color: #2b6b45; }
.card-blue   { color: #3aa7c9; }
.card-yellow { color: #f1953a; }

.quote { font-size: 18px; line-height: 1.6; }
.meta::before {
  content: "";
  display: block;
  width: 180px;
  height: 4px;
  background: currentColor;
  margin: 16px auto;
  border-radius: 2px;
}

/* I *want* to animate between slides, but this currently doesn't work well */
.slot-inner {
  transition: transform 0.9s ease, opacity 0.9s ease;
}
<section class="carousel" aria-label="Testimonials">
  <div class="carousel-row">
    <div class="peek peek-left" data-slot="prev" aria-hidden="true"></div>
    <div class="center" data-slot="current"></div>
    <div class="peek peek-right" data-slot="next" aria-hidden="true"></div>
  </div>

  <div class="controls">
    <button type="button" data-prev aria-label="Previous">‹</button>

    <div class="dots" role="tablist" aria-label="Choose slide" data-dots></div>

    <button type="button" data-next aria-label="Next">›</button>
  </div>

  <!-- Source slides (hidden) -->
  <div class="slides" hidden>
    <article data-title="Slide 1">
      <div class="card card-green">
        <p class="quote">“Generic quote text for slide one.”</p>
        <div class="meta">
          <div class="name">Person One</div>
          <div class="sub">Category A</div>
        </div>
      </div>
    </article>

    <article data-title="Slide 2">
      <div class="card card-blue">
        <p class="quote">“Generic quote text for slide two.”</p>
        <div class="meta">
          <div class="name">Person Two</div>
          <div class="sub">Category B</div>
        </div>
      </div>
    </article>

    <article data-title="Slide 3">
      <div class="card card-yellow">
        <p class="quote">“Generic quote text for slide three.”</p>
        <div class="meta">
          <div class="name">Person Three</div>
          <div class="sub">Category C</div>
        </div>
      </div>
    </article>
  </div>
</section>

解决方案

据我理解,你的代码是在移除旧卡片并再次创建新的卡片(基本上是从DOM中移除元素并创建新元素来实现滑动效果的动画)。这会在每次幻灯片切换时触发样式重新计算、布局重新计算和重绘,对于浏览器来说比较吃力。

相反,尝试使用transform translateX来移动幻灯片,因为在元素已经就位的情况下,CSS的工作效果更好。更好的是,你可以使用像Swiperjs这样的库来实现更顺滑、优化更高的动画。

但如果你愿意自己动手,这里给出修复办法:

  • 核心问题是update() 内的这一行:
prevSlot.innerHTML = wrapPeek(slides[prevIndex].html);
currSlot.innerHTML = wrapSlot(slides[index].html);
nextSlot.innerHTML = wrapPeek(slides[nextIndex].html);
  • 要修复它:

  • 在每次导航时停止替换innerHTML

  • 与其在update() 中注入HTML,不如这样:

  • 一次性创建所有幻灯片

  • 将它们保留在DOM中
  • 只改变transform

  • 移除这一整段:

function update() {

const prevIndex = mod(index - 1, slides.length);
const nextIndex = mod(index + 1, slides.length);

prevSlot.innerHTML = wrapPeek(slides[prevIndex].html);
currSlot.innerHTML = wrapSlot(slides[index].html);
nextSlot.innerHTML = wrapPeek(slides[nextIndex].html);
renderDots();
}

改为:先一次性构建插槽

update() 之前添加以下内容:

slides.forEach(s => {
  const el = document.createElement("div");
  el.className = "slot-inner";
  el.innerHTML = s.html;
  currSlot.appendChild(el);
});

然后完全移除 wrapSlotwrapPeek 的用法。

  1. 用transform逻辑替换 update()
function update() {

  const width = currSlot.offsetWidth;

  currSlot.style.transform = `translateX(-${index * width}px)`;

  renderDots();
}
  1. 移除绝对定位。
.peek .slot-inner,
.center .slot-inner {
  position: absolute;
  inset: 0;
  width: 100%;
}
  1. 改为添加以下内容:
.center {
  overflow: hidden;
}

.center {
  display: flex;
  transition: transform 0.9s cubic-bezier(.22,.61,.36,1);
  will-change: transform;
}

.slot-inner {
  flex: 0 0 100%;
}

小贴士:直接使用类似SwiperJS的库就可以了。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章