<div><span class="lbl">Featured slide with an episode list</span>
<div id="nightfall" class="carousel slide carousel-fade nf" role="region" aria-roledescription="carousel" aria-label="Nightfall, season 2">
<div class="row g-0">
<div class="col-lg">
<div class="carousel-inner" aria-live="polite">
<div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
<img src="https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" alt="The Milky Way and a shooting star over snowy mountains">
<div class="carousel-caption"><p class="nf-tag">Episode 1 · 52 min</p><h2>Dark Sky Country</h2><p class="nf-desc">Astronomers in Chile's Atacama count the satellites crossing a sky that was empty twenty years ago.</p>
<div class="d-flex flex-wrap gap-2 nf-actions"><a class="btn rounded-pill nf-btn" href="#"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg>Play episode</a><a class="btn rounded-pill nf-ghost" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>My list</a></div></div>
</div>
<div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
<img src="https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" alt="Green northern lights above a silhouetted pine forest">
<div class="carousel-caption"><p class="nf-tag">Episode 2 · 47 min</p><h2>Green Fire</h2><p class="nf-desc">In Finnish Lapland a reindeer herder and a physicist wait out the strongest aurora season in eleven years.</p>
<div class="d-flex flex-wrap gap-2 nf-actions"><a class="btn rounded-pill nf-btn" href="#"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg>Play episode</a><a class="btn rounded-pill nf-ghost" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>My list</a></div></div>
</div>
<div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
<img src="https://images.unsplash.com/photo-1509773896068-7fd415d91e2e?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" alt="The Milky Way reflected in a still lake between dark forests">
<div class="carousel-caption"><p class="nf-tag">Episode 3 · 50 min</p><h2>Still Water</h2><p class="nf-desc">A lake in Ontario so calm that the night sky shows twice, and the volunteers who keep its shores dark.</p>
<div class="d-flex flex-wrap gap-2 nf-actions"><a class="btn rounded-pill nf-btn" href="#"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg>Play episode</a><a class="btn rounded-pill nf-ghost" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>My list</a></div></div>
</div>
<div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
<img src="https://images.unsplash.com/photo-1532767153582-b1a0e5145009?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" alt="A red full moon above glowing orange clouds">
<div class="carousel-caption"><p class="nf-tag">Episode 4 · 44 min</p><h2>Blood Moon</h2><p class="nf-desc">Eclipse chasers cross three countries in four days for 82 minutes of red moon.</p>
<div class="d-flex flex-wrap gap-2 nf-actions"><a class="btn rounded-pill nf-btn" href="#"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg>Play episode</a><a class="btn rounded-pill nf-ghost" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>My list</a></div></div>
</div>
</div>
</div>
<div class="col-lg-auto nf-side">
<h3>Nightfall · Season 2</h3>
<div class="carousel-indicators">
<button type="button" data-bs-target="#nightfall" data-bs-slide-to="0" class="active" aria-current="true"><span class="nf-num">01</span><span class="nf-title">Dark Sky Country</span><span class="nf-mins">52 min</span></button>
<button type="button" data-bs-target="#nightfall" data-bs-slide-to="1"><span class="nf-num">02</span><span class="nf-title">Green Fire</span><span class="nf-mins">47 min</span></button>
<button type="button" data-bs-target="#nightfall" data-bs-slide-to="2"><span class="nf-num">03</span><span class="nf-title">Still Water</span><span class="nf-mins">50 min</span></button>
<button type="button" data-bs-target="#nightfall" data-bs-slide-to="3"><span class="nf-num">04</span><span class="nf-title">Blood Moon</span><span class="nf-mins">44 min</span></button>
</div>
</div>
</div>
</div></div>
<div><span class="lbl">Centre mode with dimmed neighbours</span>
<section class="spot" role="region" aria-roledescription="carousel" aria-label="Night Sky Photo Awards 2026" data-carousel>
<div class="spot-track" data-track tabindex="0">
<div class="spot-slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
<img src="https://images.unsplash.com/photo-1444703686981-a3abbc4d4fe3?auto=format&fit=crop&w=1200&h=750&q=70" width="1200" height="750" alt="A person standing under a bright arc of the Milky Way">
<div class="spot-cap"><span class="badge rounded-pill spot-tag">Winner · Night landscape</span><h3>Standing Room</h3><p>Lena Vogt · Namib Desert</p></div>
</div>
<div class="spot-slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
<img src="https://images.unsplash.com/photo-1531366936337-7c912a4589a7?auto=format&fit=crop&w=1200&h=750&q=70" width="1200" height="750" alt="Purple and green aurora above a snowy valley">
<div class="spot-cap"><span class="badge rounded-pill spot-tag">Winner · Aurora</span><h3>First Light</h3><p>Arvid Holm · Tromsø</p></div>
</div>
<div class="spot-slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
<img src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=1200&h=750&q=70" width="1200" height="750" loading="lazy" alt="A meteor streaking across a purple starry sky above mountains">
<div class="spot-cap"><span class="badge rounded-pill spot-tag">Runner-up · Night landscape</span><h3>Perseid, 2:14 am</h3><p>Ana Ruiz · La Palma</p></div>
</div>
<div class="spot-slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
<img src="https://images.unsplash.com/photo-1502134249126-9f3755a50d78?auto=format&fit=crop&w=1200&h=750&q=70" width="1200" height="750" loading="lazy" alt="The bright core of the Milky Way with dust lanes and stars">
<div class="spot-cap"><span class="badge rounded-pill spot-tag">Runner-up · Deep sky</span><h3>Core</h3><p>Priya Nair · Atacama</p></div>
</div>
</div>
<div class="spot-nav d-flex align-items-center justify-content-center" data-controls hidden>
<button class="btn rounded-circle spot-arrow" type="button" data-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
<div class="spot-dots d-flex gap-1">
<button type="button" data-dot aria-label="Show photo 1"></button>
<button type="button" data-dot aria-label="Show photo 2"></button>
<button type="button" data-dot aria-label="Show photo 3"></button>
<button type="button" data-dot aria-label="Show photo 4"></button>
</div>
<button class="btn rounded-circle spot-arrow" type="button" data-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
</div>
</section></div>
CSS
/* Dark carousels with Bootstrap's colour mode (data-bs-theme="dark"). The first is a .carousel-fade whose
indicators are an episode list; the second is a scroll-snap row, because .carousel shows one slide at a time. */
.nf{--nf-bg:#151922;--nf-ink:#e8eaf0;--nf-muted:#9aa3b5;--nf-accent:#a78bfa;border-radius:20px;overflow:hidden;background:var(--nf-bg);border:1px solid #232937}
.nf .carousel-inner,.nf .carousel-item{height:100%}
.nf .carousel-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.nf .carousel-item{min-height:460px}
.nf .carousel-item::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(11,13,18,.95),rgba(11,13,18,.6) 45%,rgba(11,13,18,0) 80%)}
.nf .carousel-caption{z-index:1;left:32px;right:32px;bottom:30px;max-width:520px;padding:0;text-align:left}
.nf-tag{margin:0;font-size:.78rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--nf-accent)}
.nf .carousel-caption h2{margin:.4rem 0 .5rem;font-size:clamp(1.7rem,3.4vw,2.5rem);font-weight:700;letter-spacing:-.025em;color:#fff}
.nf-desc{margin:0;line-height:1.55;color:#d3d7e2}
.nf-actions{margin-top:1.1rem}
.nf .btn{--bs-btn-padding-x:1.15rem;--bs-btn-padding-y:.7rem;--bs-btn-font-size:.9rem;--bs-btn-font-weight:700;--bs-btn-line-height:normal;--bs-btn-border-width:0;
display:inline-flex;align-items:center;gap:8px}
.nf-btn{--bs-btn-bg:var(--nf-accent);--bs-btn-color:#0b0d12;--bs-btn-hover-bg:#c4b5fd;--bs-btn-hover-color:#0b0d12;--bs-btn-active-bg:#c4b5fd;--bs-btn-active-color:#0b0d12}
.nf-ghost{--bs-btn-bg:rgba(255,255,255,.12);--bs-btn-color:#fff;--bs-btn-hover-bg:rgba(255,255,255,.22);--bs-btn-hover-color:#fff;--bs-btn-active-bg:rgba(255,255,255,.22);box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.55)}
.nf .btn svg{width:16px;height:16px}
.nf-side{padding:22px 16px;border-left:1px solid #232937}
@media (min-width:992px){.nf .nf-side{flex:0 0 300px}}
.nf-side h3{margin:0;padding:0 12px 12px;font-size:.78rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--nf-muted)}
.nf .carousel-indicators{position:static;display:grid;grid-template-columns:1fr;gap:4px;margin:0}
.nf .carousel-indicators [data-bs-target]{box-sizing:border-box;width:100%;height:auto;display:grid;grid-template-columns:auto 1fr;gap:2px 14px;margin:0;padding:13px 12px;border:0;border-radius:12px;
background:none;color:var(--nf-ink);opacity:1;text-indent:0;text-align:left;transition:background .2s,box-shadow .2s}
.nf .carousel-indicators [data-bs-target]:hover{background:#1b202b}
.nf .carousel-indicators .active{background:#1f2430;box-shadow:inset 3px 0 0 var(--nf-accent)}
.nf-num{grid-row:span 2;font-size:.85rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--nf-muted);padding-top:2px}
.nf .carousel-indicators .active .nf-num{color:var(--nf-accent)}
.nf-title{font-weight:600}
.nf-mins{font-size:.82rem;color:var(--nf-muted)}
.nf .btn:focus-visible,.nf .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid var(--nf-accent);outline-offset:2px;box-shadow:none}
@media (max-width:991px){.nf-side{border-left:0;border-top:1px solid #232937}.nf .carousel-caption{left:22px;right:22px;bottom:24px}}
/* Centre mode: the current photo in the middle, its neighbours dimmed at the edges */
[data-controls][hidden]{display:none!important}
.spot{--spot-accent:#a78bfa;--spot-side:18%}
.spot-track{display:flex;gap:16px;padding:6px var(--spot-side);overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.spot.is-ready .spot-track{scrollbar-width:none}
.spot-slide{position:relative;flex:0 0 100%;aspect-ratio:16/10;border-radius:18px;overflow:hidden;scroll-snap-align:center;scroll-snap-stop:always;transition:filter .35s,transform .35s}
.spot.is-ready .spot-slide:not([data-current]){filter:brightness(.4) saturate(.6);transform:scale(.93)}
.spot-slide img{width:100%;height:100%;object-fit:cover;display:block}
.spot-slide::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(11,13,18,.9),rgba(11,13,18,0) 55%)}
.spot-cap{position:absolute;z-index:1;left:24px;right:24px;bottom:20px;color:#fff}
.spot-tag{--bs-badge-padding-x:10px;--bs-badge-padding-y:4px;--bs-badge-font-size:.75rem;--bs-badge-color:#ddd6fe;background:rgba(167,139,250,.24)}
.spot-cap h3{margin:8px 0 0;font-size:1.35rem;font-weight:700;letter-spacing:-.015em}
.spot-cap p{margin:2px 0 0;font-size:.9rem;color:#d3d7e2}
.spot-nav{gap:14px;margin-top:18px}
.spot-arrow{--bs-btn-color:#e8eaf0;--bs-btn-border-color:#4b5264;--bs-btn-border-width:1.5px;--bs-btn-hover-bg:#1f2430;--bs-btn-hover-border-color:#e8eaf0;--bs-btn-hover-color:#fff;
--bs-btn-active-bg:#1f2430;--bs-btn-active-color:#fff;width:44px;height:44px;padding:0;display:grid;place-items:center}
.spot-arrow svg{width:18px;height:18px}
.spot-dots button{width:24px;height:24px;display:grid;place-items:center;padding:0;border:0;background:none}
.spot-dots button::before{content:"";width:8px;height:8px;border-radius:50%;background:#5b6275;transition:background .2s,transform .2s}
.spot-dots button[aria-current=true]::before{background:var(--spot-accent);transform:scale(1.3)}
.spot-arrow:focus-visible,.spot-dots button:focus-visible{outline:3px solid var(--spot-accent);outline-offset:2px;box-shadow:none}
.spot-track:focus-visible{outline:3px solid var(--spot-accent);outline-offset:-3px;border-radius:20px}
@media (max-width:640px){.spot{--spot-side:7%}.spot-track{gap:10px}.spot-cap{left:16px;right:16px;bottom:14px}.spot-cap h3{font-size:1.1rem}}
@media (prefers-reduced-motion:reduce){.nf .carousel-indicators [data-bs-target],.spot-slide,.spot-dots button::before{transition:none}
.spot.is-ready .spot-slide:not([data-current]){transform:none}}
JavaScript
// The episode carousel is Bootstrap's own; create it up front so the arrow keys work before the first click.
document.querySelectorAll('.carousel').forEach((el) => bootstrap.Carousel.getOrCreateInstance(el));
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
const track = root.querySelector('[data-track]');
const slides = [...track.children];
const dots = [...root.querySelectorAll('[data-dot]')];
const prev = root.querySelector('[data-prev]');
const next = root.querySelector('[data-next]');
const loop = root.hasAttribute('data-loop');
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
const current = () => Math.round(track.scrollLeft / step());
const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
const move = (dir) => {
if (loop && dir > 0 && atEnd()) return go(0);
if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
go(current() + dir * perView);
};
const sync = () => {
const i = current();
dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
slides.forEach((slide, n) => slide.toggleAttribute('data-current', n === i));
if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
if (!loop && next) next.setAttribute('aria-disabled', atEnd());
};
prev?.addEventListener('click', () => move(-1));
next?.addEventListener('click', () => move(1));
dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
root.classList.add('is-ready');
sync();
});
About this set
Carousels for dark interfaces such as streaming, music and photography sites. Featured slide with an episode list shows a documentary series: a large night-sky photo with the episode title, a short description and Play and My list buttons, and beside it a numbered list of episodes with their running times that works as the navigation, with a violet bar on the current one. On narrow screens the list moves under the photo. Centre mode with dimmed neighbours puts the current photo from a night photography award in the middle, with the previous and next ones peeking in at the edges, darkened and slightly smaller, and arrows and dots below. The palette keeps muted text at about 7:1 and the dots above 3:1 against the near-black page, so nothing turns to mud. The plain CSS and Tailwind versions use scroll-snap tracks and a short script that marks the current slide. The Bootstrap version runs in Bootstrap’s dark colour mode, builds the first as a .carousel-fade whose indicators are the episode list, and the centre mode as a scroll-snap row, because .carousel shows one slide at a time.
What’s included
Episode list that doubles as the carousel's navigation
Centre mode with the neighbouring slides dimmed and scaled down
Muted text and dots checked for contrast on a near-black background
Bootstrap version uses data-bs-theme="dark" and .carousel-fade
Slides swipe without JavaScript in the CSS and Tailwind versions
Accessibility
Both carousels are labelled regions with slides announced as "2 of 4". The episode buttons are named by their visible text, number, title and running time, and the current one has aria-current; the centre-mode arrows and dots are named buttons. Violet 3px outlines mark keyboard focus, and under prefers-reduced-motion the dimming stays but the scaling and transitions are removed.
Customise it
Change --nf-bg, --nf-ink, --nf-muted and --nf-accent on .nf, and --spot-accent and --spot-side on .spot, where --spot-side sets how much of each neighbour shows. In Tailwind these are the --color-nf-* theme values and the --side arbitrary property.