如何让我的轮播切换更平滑?
我在做一个轮播,展示如下:
- 居中且处于“活动中”状态的卡片
- 左侧和右侧的“窥视”相邻卡片(裁剪/遮罩)
- 上一页/下一页按钮+ 指示点
功能上它能够正常工作,但动画没有像本页的“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);
});
然后完全移除 wrapSlot 和 wrapPeek 的用法。
- 用transform逻辑替换
update():
function update() {
const width = currSlot.offsetWidth;
currSlot.style.transform = `translateX(-${index * width}px)`;
renderDots();
}
- 移除绝对定位。
.peek .slot-inner,
.center .slot-inner {
position: absolute;
inset: 0;
width: 100%;
}
- 改为添加以下内容:
.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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。