Hero with Auto-Rotating Background Carousel — Free HTML CSS JS Snippet
Hero with Auto-Rotating Background Carousel · Heroes · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<section class="rbg-hero" id="rbgHero">
<div class="rbg-layer rbg-layer-active" data-bg="1"></div>
<div class="rbg-layer" data-bg="2"></div>
<div class="rbg-layer" data-bg="3"></div>
<div class="rbg-layer" data-bg="4"></div>
<div class="rbg-scrim"></div>
<div class="rbg-content">
<span class="rbg-eyebrow">Destinations, reimagined</span>
<h1 class="rbg-h1">Wherever you're<br>going, go further</h1>
<p class="rbg-sub">Curated stays across four continents. Every place, one booking flow.</p>
<a href="#" class="rbg-cta">Explore destinations</a>
</div>
<div class="rbg-dots" id="rbgDots">
<button class="rbg-dot rbg-dot-active" data-index="0" aria-label="Show slide 1"></button>
<button class="rbg-dot" data-index="1" aria-label="Show slide 2"></button>
<button class="rbg-dot" data-index="2" aria-label="Show slide 3"></button>
<button class="rbg-dot" data-index="3" aria-label="Show slide 4"></button>
</div>
</section>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0b0f;color:#fff}
.rbg-hero{position:relative;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden}
.rbg-layer{position:absolute;inset:0;opacity:0;transition:opacity 1.4s ease;background-size:cover}
.rbg-layer-active{opacity:1}
.rbg-layer[data-bg="1"]{background:linear-gradient(135deg,#0f2027,#203a43 50%,#2c5364)}
.rbg-layer[data-bg="2"]{background:linear-gradient(135deg,#3a1c71,#d76d77 55%,#ffaf7b)}
.rbg-layer[data-bg="3"]{background:linear-gradient(135deg,#134e5e,#71b280)}
.rbg-layer[data-bg="4"]{background:linear-gradient(135deg,#1e3c72,#2a5298 60%,#7303c0)}
.rbg-scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.35),rgba(0,0,0,.55));z-index:1}
.rbg-content{position:relative;z-index:2;text-align:center;display:flex;flex-direction:column;align-items:center;gap:16px;padding:24px;max-width:640px}
.rbg-eyebrow{font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#fde68a}
.rbg-h1{font-size:clamp(32px,6vw,60px);font-weight:800;line-height:1.1;letter-spacing:-.02em;text-shadow:0 4px 24px rgba(0,0,0,.3)}
.rbg-sub{font-size:16px;color:#e2e8f0;line-height:1.7;max-width:460px}
.rbg-cta{margin-top:6px;background:#fff;color:#1a1a1a;font-weight:700;font-size:15px;padding:13px 28px;border-radius:9px;text-decoration:none;transition:transform .15s}
.rbg-cta:hover{transform:translateY(-2px)}
.rbg-dots{position:absolute;bottom:32px;left:50%;transform:translateX(-50%);z-index:2;display:flex;gap:10px}
.rbg-dot{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.35);border:none;cursor:pointer;padding:0;transition:background .2s,transform .2s}
.rbg-dot-active{background:#fff;transform:scale(1.25)}// Real cross-fading background carousel: a timer advances the active layer, and dots reflect + control real state.
const layers = Array.from(document.querySelectorAll('.rbg-layer'));
const dots = Array.from(document.querySelectorAll('.rbg-dot'));
const hero = document.getElementById('rbgHero');
const INTERVAL_MS = 4200;
let activeIndex = 0;
let timerId = null;
function goTo(index) {
layers[activeIndex].classList.remove('rbg-layer-active');
dots[activeIndex].classList.remove('rbg-dot-active');
activeIndex = (index + layers.length) % layers.length;
layers[activeIndex].classList.add('rbg-layer-active');
dots[activeIndex].classList.add('rbg-dot-active');
}
function next() {
goTo(activeIndex + 1);
}
function startTimer() {
clearInterval(timerId);
timerId = setInterval(next, INTERVAL_MS);
}
dots.forEach((dot) => {
dot.addEventListener('click', () => {
const index = parseInt(dot.dataset.index, 10);
goTo(index);
startTimer(); // restart the cycle so manual selection doesn't get overridden a moment later
});
});
// Pause the rotation while the hero isn't visible, so it doesn't burn cycles off-screen.
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) startTimer();
else clearInterval(timerId);
});
}, { threshold: 0.1 });
observer.observe(hero);
} else {
startTimer();
}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
Hero with Auto-Rotating Background Carousel — Timed Cross-Fade with Clickable Dots

Travel, hospitality, and real-estate hero sections often need to show range — multiple destinations or properties — without a full image slider UI competing with the headline. This snippet keeps the headline and CTA fixed in place while four full-bleed background layers cross-fade on a real timer beneath them, with indicator dots that both reflect and control the current slide.
Stacked layers, not a sliding track
All four .rbg-layer elements are absolutely positioned and stacked directly on top of each other (position: absolute; inset: 0) rather than laid out side-by-side in a scrolling track. Only the active layer has opacity: 1; every other layer sits at opacity: 0 beneath it. Switching slides means toggling one class on the outgoing layer and one on the incoming layer — the CSS transition: opacity 1.4s handles the actual cross-fade, so the "animation" is really just two class toggles plus a CSS transition, kept simple and GPU-cheap.
A real timer driving real state
activeIndex is a genuine piece of JavaScript state, advanced by setInterval(next, 4200). goTo(index) removes the active classes from the current layer and dot, computes the next index with a modulo wrap (so it cycles 0→1→2→3→0 forever), and adds the active classes to the new layer and dot — headline and CTA in .rbg-content are never touched, since they sit in a separate z-index: 2 layer above the scrim.
Dots that are real controls, not just indicators
Each dot is a <button> with a data-index; clicking one calls goTo() directly with that index — so a visitor can jump straight to slide 3 rather than waiting for the auto-rotation to arrive there. Critically, the click handler also calls startTimer() again, which clears and re-creates the interval — without this, a manual click would still get silently overridden by an in-flight timer a moment later, which is the classic bug in hand-rolled carousels.
Pausing when off-screen
An IntersectionObserver on the hero section starts the interval when the hero is visible and clears it when it scrolls out of view — so the carousel isn't burning timer cycles (and the associated reflows/repaints) on a hero the visitor has already scrolled past.
A scrim for guaranteed text contrast
A fixed .rbg-scrim gradient sits above all four background layers but below the content, so headline legibility never depends on which background happens to be active — it's darkened consistently regardless of the underlying gradient's brightness.
Customizing it
Swap the CSS gradients for real background-image photos, adjust INTERVAL_MS, or add/remove layers and dots in matching pairs (the JS reads both lists by index, so counts must stay equal). Pair it with video bg hero for a single moving background instead, or portfolio hero for a static alternative.
Step by step
How to Use
- 1Paste HTML, CSS, and JSThe first background is active; rotation starts automatically.
- 2Watch it cross-fadeEvery 4.2s the active layer and dot advance, wrapping after the last slide.
- 3Click a dot directlyJumps to that slide immediately and restarts the auto-rotation timer.
- 4Scroll the hero out of viewThe IntersectionObserver pauses the interval until it's visible again.
- 5Swap gradients for real photosReplace each .rbg-layer's background with background-image + cover.
- 6Add a fifth slideAdd matching .rbg-layer and .rbg-dot elements — indices must line up.
Real-world uses
Common Use Cases
setInterval timer.Got questions?
Frequently Asked Questions
A cross-fade. All background layers are stacked directly on top of each other with position: absolute; inset: 0, and only the active one has opacity: 1. Switching slides toggles the active class on the outgoing and incoming layers, and the CSS opacity transition handles the visual fade — nothing physically slides or translates.
goTo() jumps immediately to the clicked slide's index, and the click handler also calls startTimer() again, which clears the existing interval and creates a fresh one. This matters because without restarting the timer, the in-flight interval from before the click would still fire on its old schedule and could immediately override the slide you just manually selected.
No — an IntersectionObserver watches the hero section and calls clearInterval() when it's no longer intersecting the viewport, then calls startTimer() again once it re-enters view. This avoids burning timer cycles and layout work on a background rotation the visitor isn't currently looking at.
Replace each .rbg-layer[data-bg="N"] rule's background value with background-image: url(...); background-size: cover; background-position: center — the opacity-based cross-fade logic in the JavaScript is completely independent of what's rendered inside each layer, so no JS changes are needed.
Add a fifth .rbg-layer div with a matching data-bg value and CSS background rule, and a fifth .rbg-dot button with data-index="4" and an aria-label. Since the JavaScript reads both the .rbg-layer and .rbg-dot node lists by array index and computes wraparound with layers.length, both counts must stay equal but no other logic changes are required.