Video — Reel Carousel
A cinematic reel carousel with horizontally snapping film cards, an active card that autoplays a looping preview while its peers dim behind letterbox bars, prev and next controls, keyboard and swipe navigation, progress dots, a live timecode readout in monospace, and an amber category label that tracks the selection.
MCP
Code
:root {
--bg: #0a0a0b;
--surface: #141416;
--surface-2: #1c1c1f;
--amber: #ffb020;
--amber-d: #e6971a;
--red: #ff4d4d;
--ink: #f4f4f6;
--muted: #8a8a92;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--mono: "JetBrains Mono", ui-monospace, monospace;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 50% -10%, rgba(255, 176, 32, 0.08), transparent 60%),
var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.5;
display: flex;
align-items: center;
justify-content: center;
padding: 28px 16px;
}
.stage {
width: min(1040px, 100%);
background: linear-gradient(180deg, var(--surface), #0e0e10);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.8);
position: relative;
overflow: hidden;
}
/* ---------- header ---------- */
.stage__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--amber);
box-shadow: 0 0 0 4px rgba(255, 176, 32, 0.15);
}
.brand__name {
font-weight: 800;
letter-spacing: 0.18em;
font-size: 15px;
}
.brand__sub {
color: var(--muted);
font-size: 12.5px;
font-weight: 500;
padding-left: 10px;
margin-left: 2px;
border-left: 1px solid var(--line);
}
.head__meta {
display: flex;
align-items: center;
gap: 12px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
padding: 6px 11px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--muted);
}
.pill--live {
color: var(--ink);
}
.pill__pulse {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--red);
box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.6);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.5); }
70% { box-shadow: 0 0 0 8px rgba(255, 77, 77, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0); }
}
.tc {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
color: var(--amber);
letter-spacing: 0.04em;
padding: 6px 10px;
border-radius: var(--r-sm);
background: rgba(255, 176, 32, 0.08);
border: 1px solid rgba(255, 176, 32, 0.25);
font-variant-numeric: tabular-nums;
}
/* ---------- reel ---------- */
.reel {
position: relative;
padding: 10px 0 6px;
}
.reel__hero {
position: absolute;
inset: 0 0 auto 0;
height: 100%;
pointer-events: none;
z-index: 0;
}
.bar {
position: absolute;
left: 0;
right: 0;
height: 26px;
background: #000;
z-index: 3;
}
.bar--top {
top: 0;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.bar--bottom {
bottom: 0;
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.5);
}
.track {
list-style: none;
margin: 0;
padding: 44px calc(50% - 155px);
display: flex;
gap: 18px;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
position: relative;
z-index: 1;
outline: none;
}
.track::-webkit-scrollbar {
display: none;
}
.track:focus-visible {
box-shadow: inset 0 0 0 2px rgba(255, 176, 32, 0.5);
border-radius: var(--r-md);
}
.card {
scroll-snap-align: center;
flex: 0 0 310px;
height: 200px;
border-radius: var(--r-md);
position: relative;
overflow: hidden;
cursor: pointer;
border: 1px solid var(--line);
background: var(--surface-2);
transform: scale(0.86);
opacity: 0.5;
filter: saturate(0.6) brightness(0.7);
transition:
transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
opacity 0.4s ease,
filter 0.4s ease,
box-shadow 0.4s ease;
}
.card::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 30%, rgba(0, 0, 0, 0.75));
z-index: 1;
}
.card__poster {
position: absolute;
inset: 0;
z-index: 0;
}
/* animated shimmer that only runs on active card */
.card__poster::before {
content: "";
position: absolute;
inset: -40%;
background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.14) 50%, transparent 60%);
transform: translateX(-60%);
opacity: 0;
}
.card.is-active {
transform: scale(1);
opacity: 1;
filter: none;
box-shadow:
0 0 0 1px rgba(255, 176, 32, 0.55),
0 24px 50px -18px rgba(0, 0, 0, 0.9);
}
.card.is-active .card__poster::before {
opacity: 1;
animation: sweep 3.2s linear infinite;
}
@keyframes sweep {
0% { transform: translateX(-60%); }
100% { transform: translateX(160%); }
}
.card:hover:not(.is-active) {
opacity: 0.72;
filter: saturate(0.8) brightness(0.85);
transform: scale(0.9);
}
.card:focus-visible {
outline: 2px solid var(--amber);
outline-offset: 3px;
}
/* poster gradients per index */
.g0 { background: linear-gradient(135deg, #24344d, #0b1622 70%), radial-gradient(circle at 70% 20%, rgba(255,176,32,0.35), transparent 55%); }
.g1 { background: linear-gradient(135deg, #4a1f2b, #180a10 70%); }
.g2 { background: linear-gradient(135deg, #1f3a34, #0a1613 70%); }
.g3 { background: linear-gradient(135deg, #3a2f18, #17130a 70%); }
.g4 { background: linear-gradient(135deg, #2a2140, #0f0b1a 70%); }
.g5 { background: linear-gradient(135deg, #123040, #08161c 70%); }
.card__top {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
display: flex;
justify-content: space-between;
padding: 12px;
}
.chip {
font-family: var(--mono);
font-size: 10.5px;
font-weight: 600;
padding: 3px 7px;
border-radius: var(--r-sm);
background: rgba(0, 0, 0, 0.5);
border: 1px solid var(--line-2);
backdrop-filter: blur(4px);
letter-spacing: 0.03em;
}
.chip--res {
color: var(--amber);
}
.card__play {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(10, 10, 11, 0.55);
border: 1.5px solid rgba(255, 255, 255, 0.7);
display: grid;
place-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
z-index: 2;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.card__play svg {
width: 16px;
height: 16px;
fill: var(--white);
margin-left: 2px;
}
.card.is-active .card__play {
opacity: 0;
}
.card:not(.is-active):hover .card__play {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.card__bottom {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
padding: 14px;
}
.card__cat {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--amber);
margin-bottom: 4px;
}
.card__name {
font-size: 16px;
font-weight: 700;
margin: 0;
color: var(--white);
}
.card__dir {
font-size: 12px;
color: var(--muted);
margin: 2px 0 0;
}
/* progress line inside active card */
.card__progress {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 0;
background: linear-gradient(90deg, var(--amber-d), var(--amber));
z-index: 3;
transition: width 0.15s linear;
}
/* ---------- nav ---------- */
.nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 4;
width: 42px;
height: 42px;
border-radius: 50%;
border: 1px solid var(--line-2);
background: rgba(20, 20, 22, 0.7);
backdrop-filter: blur(8px);
color: var(--ink);
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}
.nav svg {
width: 20px;
height: 20px;
}
.nav:hover {
background: var(--amber);
color: #17130a;
border-color: var(--amber);
}
.nav:active {
transform: translateY(-50%) scale(0.92);
}
.nav:focus-visible {
outline: 2px solid var(--amber);
outline-offset: 2px;
}
.nav--prev { left: 8px; }
.nav--next { right: 8px; }
/* ---------- footer ---------- */
.stage__foot {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.foot__info {
min-height: 62px;
}
.cat {
display: inline-block;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--amber);
margin-bottom: 6px;
transition: opacity 0.3s ease;
}
.foot__title {
font-size: 24px;
font-weight: 800;
margin: 0;
letter-spacing: -0.01em;
}
.foot__by {
font-size: 13px;
color: var(--muted);
margin: 4px 0 0;
}
.dots {
display: flex;
gap: 8px;
align-items: center;
}
.dot {
width: 9px;
height: 9px;
border-radius: 999px;
border: none;
background: var(--line-2);
cursor: pointer;
padding: 0;
transition: width 0.3s ease, background 0.3s ease;
}
.dot.is-active {
width: 26px;
background: var(--amber);
}
.dot:focus-visible {
outline: 2px solid var(--amber);
outline-offset: 2px;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 20px);
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
border-left: 3px solid var(--amber);
padding: 11px 16px;
border-radius: var(--r-md);
font-size: 13px;
font-weight: 500;
box-shadow: 0 18px 40px -14px rgba(0, 0, 0, 0.8);
opacity: 0;
pointer-events: none;
transition: opacity 0.28s ease, transform 0.28s ease;
z-index: 20;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body {
padding: 14px 10px;
}
.stage {
padding: 16px;
}
.brand__sub {
display: none;
}
.head__meta .pill--live {
display: none;
}
.track {
padding: 40px calc(50% - 130px);
gap: 14px;
}
.card {
flex-basis: 260px;
height: 176px;
}
.nav {
width: 36px;
height: 36px;
}
.nav--prev { left: 2px; }
.nav--next { right: 2px; }
.stage__foot {
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
.foot__title {
font-size: 20px;
}
.foot__info {
min-height: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.card.is-active .card__poster::before,
.pill__pulse {
animation: none !important;
}
.track {
scroll-behavior: auto;
}
}(function () {
"use strict";
var REELS = [
{ name: "Neon Harbor", cat: "Commercial", dir: "Mara Vinstin", year: 2026, dur: "0:48", res: "4K · DCI", g: "g0", secs: 48 },
{ name: "Paper Cranes", cat: "Short Film", dir: "Idris Fontaine", year: 2025, dur: "6:12", res: "2K · ProRes", g: "g1", secs: 372 },
{ name: "Static Bloom", cat: "Music Video", dir: "Lena Ortuño", year: 2026, dur: "3:31", res: "6K · RAW", g: "g2", secs: 211 },
{ name: "The Long Field", cat: "Documentary", dir: "Corey Alvane", year: 2024, dur: "12:04", res: "4K · LOG", g: "g3", secs: 724 },
{ name: "Copper Hour", cat: "Fashion", dir: "Nadia Brecht", year: 2026, dur: "1:20", res: "4K · DCI", g: "g4", secs: 80 },
{ name: "Undertow", cat: "Title Sequence", dir: "Sami Kolt", year: 2025, dur: "0:55", res: "5K · RAW", g: "g5", secs: 55 },
];
var track = document.getElementById("track");
var dotsWrap = document.getElementById("dots");
var prevBtn = document.getElementById("prev");
var nextBtn = document.getElementById("next");
var tcEl = document.getElementById("timecode");
var catEl = document.getElementById("active-cat");
var titleEl = document.getElementById("active-title");
var byEl = document.getElementById("active-by");
var toastEl = document.getElementById("toast");
var cards = [];
var dots = [];
var active = 0;
var tick = null;
var frame = 0;
var scrollRaf = null;
var toastTimer = null;
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function playIcon() {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>';
}
function build() {
REELS.forEach(function (r, i) {
var li = document.createElement("li");
li.className = "card " + (i === 0 ? "is-active" : "");
li.setAttribute("role", "option");
li.setAttribute("aria-selected", i === 0 ? "true" : "false");
li.setAttribute("aria-label", r.name + ", " + r.cat + ", " + r.dur);
li.tabIndex = -1;
li.innerHTML =
'<div class="card__poster ' + r.g + '"></div>' +
'<div class="card__top">' +
'<span class="chip">' + r.dur + '</span>' +
'<span class="chip chip--res">' + r.res + '</span>' +
'</div>' +
'<div class="card__play">' + playIcon() + '</div>' +
'<div class="card__bottom">' +
'<div class="card__cat">' + r.cat + '</div>' +
'<h3 class="card__name">' + r.name + '</h3>' +
'<p class="card__dir">Dir. ' + r.dir + ' · ' + r.year + '</p>' +
'</div>' +
'<div class="card__progress"></div>';
li.addEventListener("click", function () {
if (i === active) {
toast("Added " + r.name + " to shortlist");
} else {
goTo(i, true);
}
});
track.appendChild(li);
cards.push(li);
var dot = document.createElement("button");
dot.className = "dot " + (i === 0 ? "is-active" : "");
dot.type = "button";
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Go to " + r.name);
dot.setAttribute("aria-selected", i === 0 ? "true" : "false");
dot.addEventListener("click", function () {
goTo(i, true);
});
dotsWrap.appendChild(dot);
dots.push(dot);
});
}
function fmtTC(elapsed) {
var f = Math.floor((elapsed % 1) * 24);
var total = Math.floor(elapsed);
var h = Math.floor(total / 3600);
var m = Math.floor((total % 3600) / 60);
var s = total % 60;
return (
pad(h) + ":" + pad(m) + ":" + pad(s) + ":" + pad(f)
);
}
function pad(n) {
return (n < 10 ? "0" : "") + n;
}
function startTimecode() {
stopTimecode();
var reel = REELS[active];
var start = performance.now();
var progressEl = cards[active].querySelector(".card__progress");
tick = setInterval(function () {
var elapsed = (performance.now() - start) / 1000;
// loop the preview relative to a compressed 8s window for demo feel
var window = Math.min(reel.secs, 8);
var pos = elapsed % window;
tcEl.textContent = fmtTC(pos * (reel.secs / window));
if (progressEl) {
progressEl.style.width = ((pos / window) * 100).toFixed(1) + "%";
}
}, 42);
}
function stopTimecode() {
if (tick) {
clearInterval(tick);
tick = null;
}
cards.forEach(function (c) {
var p = c.querySelector(".card__progress");
if (p) p.style.width = "0";
});
}
function render() {
cards.forEach(function (c, i) {
var on = i === active;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", on ? "true" : "false");
});
dots.forEach(function (d, i) {
var on = i === active;
d.classList.toggle("is-active", on);
d.setAttribute("aria-selected", on ? "true" : "false");
});
var r = REELS[active];
// fade footer text
catEl.style.opacity = "0";
setTimeout(function () {
catEl.textContent = r.cat;
titleEl.textContent = r.name;
byEl.textContent = "Dir. " + r.dir + " · " + r.year;
catEl.style.opacity = "1";
}, 130);
if (!reduced) startTimecode();
else {
stopTimecode();
tcEl.textContent = "00:00:00:00";
}
}
function centerCard(i, smooth) {
var card = cards[i];
var target = card.offsetLeft - (track.clientWidth - card.clientWidth) / 2;
track.scrollTo({ left: target, behavior: smooth ? "smooth" : "auto" });
}
function goTo(i, smooth) {
active = (i + REELS.length) % REELS.length;
centerCard(active, smooth);
render();
}
// keep active in sync while user scrolls / swipes
function onScroll() {
if (scrollRaf) return;
scrollRaf = requestAnimationFrame(function () {
scrollRaf = null;
var center = track.scrollLeft + track.clientWidth / 2;
var best = 0;
var bestDist = Infinity;
cards.forEach(function (c, i) {
var mid = c.offsetLeft + c.clientWidth / 2;
var d = Math.abs(mid - center);
if (d < bestDist) {
bestDist = d;
best = i;
}
});
if (best !== active) {
active = best;
render();
}
});
}
// events
prevBtn.addEventListener("click", function () {
goTo(active - 1, true);
});
nextBtn.addEventListener("click", function () {
goTo(active + 1, true);
});
track.addEventListener("scroll", onScroll, { passive: true });
track.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") {
e.preventDefault();
goTo(active + 1, true);
} else if (e.key === "ArrowLeft") {
e.preventDefault();
goTo(active - 1, true);
} else if (e.key === "Home") {
e.preventDefault();
goTo(0, true);
} else if (e.key === "End") {
e.preventDefault();
goTo(REELS.length - 1, true);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toast("Added " + REELS[active].name + " to shortlist");
}
});
window.addEventListener("resize", function () {
centerCard(active, false);
});
build();
// initial position after layout settles
requestAnimationFrame(function () {
centerCard(0, false);
render();
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reel Carousel — Showreel</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<header class="stage__head">
<div class="brand">
<span class="brand__dot" aria-hidden="true"></span>
<span class="brand__name">NORTHLIGHT</span>
<span class="brand__sub">Directors' Showreel</span>
</div>
<div class="head__meta">
<span class="pill pill--live"><span class="pill__pulse" aria-hidden="true"></span>Now screening</span>
<span class="tc" id="timecode" aria-label="Preview timecode">00:00:00:00</span>
</div>
</header>
<section class="reel" aria-roledescription="carousel" aria-label="Featured reels">
<div class="reel__hero" aria-hidden="true">
<span class="bar bar--top"></span>
<span class="bar bar--bottom"></span>
</div>
<button class="nav nav--prev" id="prev" type="button" aria-label="Previous reel">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="nav nav--next" id="next" type="button" aria-label="Next reel">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="track" id="track" tabindex="0" role="listbox" aria-label="Reel selection">
<!-- cards injected by script.js -->
</ul>
</section>
<footer class="stage__foot">
<div class="foot__info" id="active-info">
<span class="cat" id="active-cat">Commercial</span>
<h1 class="foot__title" id="active-title">Neon Harbor</h1>
<p class="foot__by" id="active-by">Dir. Mara Vinstin · 2026</p>
</div>
<div class="dots" id="dots" role="tablist" aria-label="Reel progress"></div>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A film-forward reel carousel built for showreels and director portfolios. A row of horizontally snapping cards scrolls under a letterboxed hero frame; the centered card becomes “active”, scaling up and starting a silent, looping preview animation while the surrounding cards recede into shadow. Each card carries a poster gradient, a runtime badge, a resolution chip, and a category label rendered in cinematic amber.
Navigation is deliberate and tactile: click a card to bring it forward, use the floating prev/next arrows, drag or swipe on touch, or drive it entirely from the keyboard with the arrow keys and Home/End. A row of progress dots mirrors the active index, and a JetBrains Mono timecode ticks upward while the active preview plays, resetting on every scene change.
The layout collapses gracefully to a single-column, swipe-first experience below 520px, keeps focus rings visible for keyboard users, and respects reduced-motion preferences by pausing the autoplay shimmer. A small toast confirms actions like adding a reel to a shortlist.