I’m building a carousel that shows:

A center “active” card

A left and right “peek” of adjacent cards (clipped/masked)

Prev/next buttons + dots

It works functionally, but the animation doesn’t feel smooth like the “Thousands of Businesses Transformed” carousel on this page. How can I mimic this behavior?
https://www.ramseysolutions.com/business/entreleadership

Right now my JS replaces innerHTML inside the slots on every navigation. This causes a “snap” / jank, and I can’t get a smooth slide/crossfade between the old and new cards. I also sometimes see a brief “extra card under” effect if two .ts-slot-inner elements exist during transitions.

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>

newuser's user avatar

2

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.