Auto-Height Carousel — HTML CSS JS Snippet
Auto-Height Carousel · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="ahc-wrap">
<div class="ahc-viewport" id="ahcViewport">
<div class="ahc-track" id="ahcTrack">
<div class="ahc-slide"><h3>Short FAQ</h3><p>Do you offer refunds? Yes, within 30 days.</p></div>
<div class="ahc-slide"><h3>A Medium One</h3><p>Our platform supports single sign-on, role-based access control, audit logs, and exports to CSV or JSON for any report you build.</p></div>
<div class="ahc-slide"><h3>The Long Answer</h3><p>Pricing scales with active seats, not total users — invited members who never log in cost nothing. Annual billing saves 20% versus monthly, and you can switch between plans at any time; unused time on a downgrade is credited to your next invoice automatically, no support ticket required.</p></div>
</div>
</div>
<div class="ahc-controls">
<button class="ahc-btn" id="ahcPrev" aria-label="Previous">‹</button>
<div class="ahc-dots" id="ahcDots"></div>
<button class="ahc-btn" id="ahcNext" aria-label="Next">›</button>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f6f7f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ahc-wrap{width:100%;max-width:380px}
.ahc-viewport{position:relative;overflow:hidden;border-radius:14px;background:#fff;box-shadow:0 10px 26px rgba(15,23,42,.14);transition:height .35s cubic-bezier(.4,0,.2,1)}
.ahc-track{display:flex;transition:transform .4s cubic-bezier(.4,0,.2,1)}
.ahc-slide{flex:0 0 100%;padding:24px;box-sizing:border-box}
.ahc-slide h3{font-size:15px;font-weight:800;color:#1f2937;margin-bottom:8px}
.ahc-slide p{font-size:13px;color:#4b5563;line-height:1.6}
.ahc-controls{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px}
.ahc-btn{width:36px;height:36px;border-radius:50%;background:#fff;border:1.5px solid #e3e5ea;color:#4b5563;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color .15s,color .15s}
.ahc-btn:hover{border-color:#6366f1;color:#6366f1}
.ahc-dots{display:flex;gap:7px}
.ahc-dot{width:7px;height:7px;border-radius:50%;background:#d1d5db;border:none;cursor:pointer;transition:background .2s,width .2s}
.ahc-dot.active{background:#6366f1;width:20px;border-radius:4px}var viewport = document.getElementById('ahcViewport');
var track = document.getElementById('ahcTrack');
var slides = document.querySelectorAll('.ahc-slide');
var dotsWrap = document.getElementById('ahcDots');
var current = 0;
slides.forEach(function (s, i) {
var d = document.createElement('button');
d.className = 'ahc-dot';
d.setAttribute('aria-label', 'Go to slide ' + (i + 1));
d.addEventListener('click', function () { goTo(i); });
dotsWrap.appendChild(d);
});
var dots = document.querySelectorAll('.ahc-dot');
function measureAndSetHeight() {
// Measuring the active slide's natural height and applying it explicitly
// to the viewport (rather than letting the viewport size to its tallest
// child, or leaving it unset) is what makes an animated height CHANGE
// possible at all — a CSS transition cannot animate to/from "auto".
var activeSlide = slides[current];
var h = activeSlide.getBoundingClientRect().height;
viewport.style.height = h + 'px';
}
function render() {
track.style.transform = 'translateX(' + (-current * 100) + '%)';
dots.forEach(function (d, i) { d.classList.toggle('active', i === current); });
measureAndSetHeight();
}
function goTo(i) { current = i; render(); }
function next() { goTo((current + 1) % slides.length); }
function prev() { goTo((current - 1 + slides.length) % slides.length); }
document.getElementById('ahcNext').addEventListener('click', next);
document.getElementById('ahcPrev').addEventListener('click', prev);
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') next();
else if (e.key === 'ArrowLeft') prev();
});
window.addEventListener('resize', measureAndSetHeight);
// Set the initial height without a transition, so the very first paint
// doesn't visibly animate from 0.
viewport.style.transition = 'none';
render();
requestAnimationFrame(function () { viewport.style.transition = ''; });Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Auto-Height Carousel — Measuring Content to Animate a Property CSS Can't Animate

CSS cannot transition height to or from auto — it's one of the oldest, most persistent limitations in the language. This carousel works around it by never using auto for the animated value at all: on every slide change, JavaScript measures the *new* active slide's real, natural height in pixels with getBoundingClientRect(), and sets that literal pixel value on the viewport. Because the viewport's height is a CSS transition-able property, animating *between two known pixel numbers* works perfectly — the trick is entirely in how that target number gets produced.
Measuring the slide, not the viewport
Each .ahc-slide sits in a flex track sized at 100% width each, so all three slides' natural heights already exist in the DOM regardless of which one is "active" — measureAndSetHeight() just reads getBoundingClientRect().height off whichever slide index is currently current. That's the one measurement this entire pattern depends on: a slide's true rendered height, taken fresh every time the active index changes.
Skipping the transition on the very first paint
If the viewport's height transition were active from the start, the browser would animate from its default (auto-derived, effectively 0 before any height is set) up to the first slide's real height — a visible pop on page load. Setting transition: none before the first render() call, then clearing it back on the next animation frame, sidesteps that: the first height is applied instantly, and only subsequent slide changes animate.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA short FAQ appears in a card sized to fit it exactly — no extra empty space.
- 2Click nextThe card smoothly grows taller as it slides to the medium-length answer.
- 3Click next againIt grows further still for the longest answer — always sized to fit exactly, never clipped or padded.
- 4Click back to the firstThe card smoothly shrinks back down to the short answer's natural height.
- 5Add a slide with different content lengthNo height needs to be set manually — measureAndSetHeight reads it automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Browsers cannot animate a transition to or from the auto keyword — there's no defined intermediate state between "auto" and a pixel value for the browser to interpolate. Measuring the real height in JavaScript and setting an explicit pixel value is the standard, reliable workaround.
Call measureAndSetHeight() again after that content finishes loading/rendering — it always re-measures the currently active slide's real height, so it stays accurate as long as it's invoked after any layout-affecting change.
Only if the height is measured after the image has actually loaded — an image without explicit width/height attributes can cause a late reflow. Add an aspect-ratio or explicit dimensions to any image, or re-measure on that image's load event.
Without that guard, the viewport would visibly animate from its unset/zero starting height up to the first slide's real height the instant the page loads — a distracting pop-in that has nothing to do with actual carousel navigation.
Arrows and dots are real labeled buttons operable via Left/Right arrow keys — for full accessibility, ensure the animated height change doesn't interfere with a screen reader's ability to read the newly active slide's content, which it won't since content is always present in the DOM, just visually repositioned.