Features / Bento
Scroll-snap feature carousel
Swipeable cards with arrows and dots
Swipeable cards with arrows and dots
Create a horizontally scrolling feature carousel with Tailwind CSS. The header row holds a title and subline on the left and two round arrow buttons on the right, hidden below sm. The track is a flex row with snap-x snap-mandatory, overflow-x-auto, scroll-smooth and a hidden scrollbar; each card is a shrink-0 snap-start article about 85% wide on mobile, 45% on sm and 31% on lg, with a rounded icon tile, a title, two lines of copy, and a subtle "Learn more →" link. Add an empty dots container under the track. Then add JavaScript that measures the first card plus the gap to scroll exactly one card per arrow press, builds one dot per card and marks the nearest one active as the track scrolls, lets a dot click jump to its card, fades out an arrow when the track is at either end, and recalculates on resize so the maths survives a breakpoint change. <section class="mx-auto max-w-6xl px-6 py-16">
<div class="flex items-end justify-between gap-6">
<div>
<h2 class="text-3xl font-semibold text-neutral-900">Built for the whole release</h2>
<p class="mt-2 text-neutral-500">Six capabilities that cover the path from commit to customer.</p>
</div>
<div class="hidden gap-2 sm:flex">
<button id="prev" class="flex h-10 w-10 items-center justify-center rounded-full border border-neutral-200 text-neutral-600 transition hover:bg-neutral-50">←</button>
<button id="next" class="flex h-10 w-10 items-center justify-center rounded-full border border-neutral-200 text-neutral-600 transition hover:bg-neutral-50">→</button>
</div>
</div>
<div id="track" class="mt-8 flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth pb-2 [-ms-overflow-style:none] [scrollbar-width:none]">
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M20 11a8 8 0 1 0-2.34 5.66"/><path d="M20 4v7h-7"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Preview environments</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">Every pull request gets a full stack with seeded data and its own URL.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Time-travel debugging</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">Replay any request against the exact build and config that served it.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m12 3 9 5-9 5-9-5 9-5Z"/><path d="m3 12 9 5 9-5M3 16l9 5 9-5"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Progressive rollouts</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">Ship to 1% of traffic, watch the error budget, promote or revert.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m8 9-3 3 3 3m8-6 3 3-3 3m-2-9-4 12"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Typed configuration</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">Schema-checked settings that fail the build, not the deploy.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Cost attribution</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">See spend per service, per branch, per customer — before the invoice.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
<article class="w-[85%] shrink-0 snap-start rounded-2xl border border-neutral-200 bg-white p-6 sm:w-[45%] lg:w-[31%]">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M12 3 4.5 6v5.5c0 4.7 3.2 7.7 7.5 9.5 4.3-1.8 7.5-4.8 7.5-9.5V6L12 3Z"/><path d="m9 12 2 2 4-4"/></svg></div>
<h3 class="mt-5 font-medium text-neutral-900">Policy guardrails</h3>
<p class="mt-2 text-sm leading-relaxed text-neutral-500">Block a risky change at review time instead of explaining it later.</p>
<a href="#" class="mt-4 inline-block text-sm font-medium text-neutral-900 hover:underline">Learn more →</a>
</article>
</div>
<div id="dots" class="mt-6 flex justify-center gap-2"></div>
</section> var track = document.getElementById("track");
var prev = document.getElementById("prev");
var next = document.getElementById("next");
var dots = document.getElementById("dots");
var cards = [].slice.call(track.children);
var ACTIVE = "h-2 w-6 rounded-full bg-neutral-900 transition-all";
var IDLE = "h-2 w-2 rounded-full bg-neutral-300 transition-all";
// One card plus the gap between two of them — the distance one arrow press moves.
function stride() {
if (cards.length < 2) return track.clientWidth;
return cards[1].offsetLeft - cards[0].offsetLeft;
}
cards.forEach(function (card, i) {
var dot = document.createElement("button");
dot.className = IDLE;
dot.setAttribute("aria-label", "Go to slide " + (i + 1));
dot.addEventListener("click", function () {
track.scrollTo({ left: card.offsetLeft - cards[0].offsetLeft, behavior: "smooth" });
});
dots.appendChild(dot);
});
function sync() {
var index = Math.round(track.scrollLeft / stride());
[].slice.call(dots.children).forEach(function (dot, i) {
dot.className = i === index ? ACTIVE : IDLE;
});
var atStart = track.scrollLeft < 4;
var atEnd = track.scrollLeft > track.scrollWidth - track.clientWidth - 4;
prev.classList.toggle("opacity-30", atStart);
next.classList.toggle("opacity-30", atEnd);
prev.disabled = atStart;
next.disabled = atEnd;
}
prev.addEventListener("click", function () { track.scrollBy({ left: -stride(), behavior: "smooth" }); });
next.addEventListener("click", function () { track.scrollBy({ left: stride(), behavior: "smooth" }); });
track.addEventListener("scroll", sync);
window.addEventListener("resize", sync);
sync();