Video — Grade Before After
A cinematic before/after color-grade comparator with a draggable split slider, letterboxed hero frame, and a rack of selectable film looks. Each LUT swaps the graded side in real time with amber-accented labels, exposure and contrast readouts, and JetBrains Mono timecodes. Keyboard-nudgeable handle, animated wipe, and copy-to-clipboard LUT names make it feel like a colorist grading bay rendered in HTML.
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;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
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: "Inter", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
justify-content: center;
padding: 32px 20px 56px;
}
.bay {
width: 100%;
max-width: 860px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Header */
.bay__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.bay__brand {
display: flex;
align-items: center;
gap: 12px;
}
.bay__brand .dot {
width: 34px;
height: 34px;
border-radius: 9px;
background: linear-gradient(140deg, var(--amber), var(--amber-d));
box-shadow: 0 0 0 1px var(--line-2), 0 6px 18px rgba(255, 176, 32, 0.25);
flex: none;
}
.bay__brand h1 {
margin: 0;
font-size: 1.15rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.bay__brand p {
margin: 0;
color: var(--muted);
font-size: 0.8rem;
}
.bay__meta {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
color: var(--muted);
font-family: "JetBrains Mono", monospace;
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.02em;
}
.chip--rec { color: var(--ink); }
.rec {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--red);
box-shadow: 0 0 8px var(--red);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Stage / letterbox */
.stage {
background: #000;
border-radius: var(--r-lg);
overflow: hidden;
border: 1px solid var(--line);
box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.9);
}
.bar {
height: 34px;
background: #000;
}
.viewer {
position: relative;
aspect-ratio: 16 / 8;
overflow: hidden;
cursor: ew-resize;
outline: none;
user-select: none;
touch-action: none;
}
.viewer:focus-visible {
box-shadow: inset 0 0 0 3px var(--amber);
}
.frame {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
/* Fictional cityscape-at-dusk plate built from gradients (no hotlinking) */
.frame--before,
.frame--after {
background-image:
linear-gradient(0deg, rgba(10, 12, 20, 0.85) 0%, transparent 45%),
linear-gradient(180deg, #5b6d86 0%, #7d8ba0 30%, #b7a58c 62%, #d8b98e 100%);
}
.frame--after {
clip-path: inset(0 0 0 50%);
filter: contrast(1.12) saturate(1.25) brightness(1.02);
transition: filter 0.45s ease;
}
.frame--after.is-wiping { transition: filter 0.2s ease, clip-path 0.35s cubic-bezier(.2,.7,.2,1); }
.tint {
position: absolute;
inset: 0;
mix-blend-mode: soft-light;
opacity: 0.55;
transition: background 0.45s ease, opacity 0.45s ease;
}
.tint--before {
background: linear-gradient(180deg, rgba(120, 130, 150, 0.4), rgba(90, 100, 120, 0.35));
opacity: 0.35;
}
.tint--after {
background: linear-gradient(180deg, rgba(255, 176, 32, 0.28), rgba(20, 40, 70, 0.4));
}
/* skyline silhouette overlay for both halves */
.viewer::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 42%;
background:
linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.55)),
repeating-linear-gradient(90deg, rgba(8, 10, 16, 0.9) 0 14px, rgba(8, 10, 16, 0.9) 14px 20px, transparent 20px 22px, transparent 22px 46px);
-webkit-mask: linear-gradient(180deg, transparent, #000 40%);
mask: linear-gradient(180deg, transparent, #000 40%);
pointer-events: none;
}
.tag {
position: absolute;
top: 12px;
z-index: 3;
padding: 5px 10px;
border-radius: var(--r-sm);
font-family: "JetBrains Mono", monospace;
font-size: 0.64rem;
font-weight: 700;
letter-spacing: 0.06em;
backdrop-filter: blur(6px);
border: 1px solid var(--line-2);
}
.tag--before {
left: 12px;
color: #d9dde6;
background: rgba(10, 10, 12, 0.6);
}
.tag--after {
right: 12px;
color: #0a0a0b;
background: var(--amber);
box-shadow: 0 4px 14px rgba(255, 176, 32, 0.35);
}
/* Handle */
.handle {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: 4;
pointer-events: none;
}
.handle__line {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 2px;
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(255, 176, 32, 0.5), var(--amber), rgba(255, 176, 32, 0.5));
box-shadow: 0 0 12px rgba(255, 176, 32, 0.5);
}
.handle__grip {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(10, 10, 11, 0.82);
border: 2px solid var(--amber);
color: var(--amber);
display: grid;
place-items: center;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
}
.viewer:focus-visible .handle__grip {
box-shadow: 0 0 0 4px rgba(255, 176, 32, 0.3), 0 6px 20px rgba(0, 0, 0, 0.6);
}
/* HUD */
.hud {
position: absolute;
left: 12px;
bottom: 12px;
right: 12px;
z-index: 3;
display: flex;
justify-content: space-between;
align-items: center;
font-family: "JetBrains Mono", monospace;
font-size: 0.66rem;
color: #eaeaf0;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.hud__tc {
background: rgba(0, 0, 0, 0.5);
padding: 3px 8px;
border-radius: var(--r-sm);
letter-spacing: 0.05em;
}
.hud__split { letter-spacing: 0.1em; color: var(--muted); }
.hud__split b { color: var(--amber); }
/* Readout */
.readout {
display: grid;
grid-template-columns: repeat(4, 1fr) 1.6fr auto;
gap: 10px;
align-items: stretch;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 14px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.stat span {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
}
.stat b {
font-family: "JetBrains Mono", monospace;
font-size: 0.95rem;
font-weight: 700;
color: var(--ink);
}
.stat--lut {
min-width: 0;
}
.stat--lut b {
font-size: 0.78rem;
color: var(--amber);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy {
align-self: stretch;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
border-radius: var(--r-md);
padding: 0 16px;
font: inherit;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, transform 0.08s;
white-space: nowrap;
}
.copy:hover { background: #26262a; border-color: var(--amber); }
.copy:active { transform: scale(0.97); }
.copy:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
/* Rack */
.rack__title {
margin: 4px 2px;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
font-weight: 700;
}
.rack__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.look {
position: relative;
text-align: left;
border: 1px solid var(--line);
background: var(--surface);
border-radius: var(--r-md);
padding: 0;
overflow: hidden;
cursor: pointer;
color: var(--ink);
font: inherit;
transition: border-color 0.15s, transform 0.1s, box-shadow 0.15s;
}
.look:hover { transform: translateY(-2px); border-color: var(--line-2); }
.look:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.look.is-active {
border-color: var(--amber);
box-shadow: 0 0 0 1px var(--amber), 0 10px 26px -12px rgba(255, 176, 32, 0.5);
}
.look__swatch {
height: 52px;
width: 100%;
}
.look__body {
padding: 9px 11px 11px;
}
.look__name {
font-size: 0.82rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.look__lut {
margin-top: 3px;
font-family: "JetBrains Mono", monospace;
font-size: 0.62rem;
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.look__check {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--amber);
color: #0a0a0b;
display: grid;
place-items: center;
font-size: 0.7rem;
font-weight: 800;
opacity: 0;
transform: scale(0.6);
transition: opacity 0.15s, transform 0.15s;
}
.look.is-active .look__check { opacity: 1; transform: scale(1); }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--amber);
color: #0a0a0b;
font-weight: 700;
font-size: 0.82rem;
padding: 10px 18px;
border-radius: 999px;
box-shadow: 0 12px 30px -8px rgba(255, 176, 32, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 20;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* Responsive */
@media (max-width: 520px) {
body { padding: 20px 14px 44px; }
.bar { height: 22px; }
.viewer { aspect-ratio: 4 / 3; }
.readout {
grid-template-columns: 1fr 1fr;
}
.stat--lut { grid-column: 1 / -1; }
.copy { grid-column: 1 / -1; padding: 11px; }
.rack__grid { grid-template-columns: 1fr 1fr; }
.tag { font-size: 0.58rem; }
}(function () {
"use strict";
// Look definitions — realistic but fictional grading presets.
var LOOKS = [
{
id: "kodak2383",
name: "Kodak 2383",
lut: "Kodak_2383_D65.cube",
swatch: "linear-gradient(120deg,#3a2f1e,#c98b3a 55%,#f4d9a0)",
filter: "contrast(1.14) saturate(1.28) brightness(1.03) sepia(.08)",
tint: "linear-gradient(180deg,rgba(255,176,32,.30),rgba(20,40,70,.42))",
exp: "+0.3", con: "+18", sat: "+22", temp: "+320K"
},
{
id: "tealnoir",
name: "Teal Noir",
lut: "Teal_Noir_Cross.cube",
swatch: "linear-gradient(120deg,#04191f,#0d5b63 55%,#e08a3c)",
filter: "contrast(1.22) saturate(1.1) brightness(.96) hue-rotate(-8deg)",
tint: "linear-gradient(180deg,rgba(12,90,110,.5),rgba(255,140,60,.22))",
exp: "-0.2", con: "+30", sat: "+8", temp: "-260K"
},
{
id: "golden",
name: "Golden Hour",
lut: "Golden_Hour_Warm.cube",
swatch: "linear-gradient(120deg,#5a2e0a,#f0a437 55%,#ffe6b0)",
filter: "contrast(1.08) saturate(1.35) brightness(1.08) sepia(.14)",
tint: "linear-gradient(180deg,rgba(255,176,32,.42),rgba(255,90,40,.28))",
exp: "+0.7", con: "+10", sat: "+30", temp: "+540K"
},
{
id: "bleach",
name: "Bleach Bypass",
lut: "Bleach_Bypass_HiCon.cube",
swatch: "linear-gradient(120deg,#3d3d40,#9a9aa0 55%,#ececef)",
filter: "contrast(1.34) saturate(.7) brightness(1.05)",
tint: "linear-gradient(180deg,rgba(210,210,215,.35),rgba(40,40,48,.4))",
exp: "+0.1", con: "+42", sat: "-30", temp: "-40K"
},
{
id: "d4n",
name: "Day-for-Night",
lut: "Day_For_Night_Blue.cube",
swatch: "linear-gradient(120deg,#050b1a,#16385f 55%,#4a6ea0)",
filter: "contrast(1.2) saturate(.9) brightness(.7) hue-rotate(-18deg)",
tint: "linear-gradient(180deg,rgba(20,50,110,.6),rgba(6,12,30,.55))",
exp: "-1.4", con: "+26", sat: "-12", temp: "-820K"
}
];
var viewer = document.getElementById("viewer");
var frameAfter = document.getElementById("frameAfter");
var handle = document.getElementById("handle");
var tint = document.getElementById("tint");
var tagAfter = document.getElementById("tagAfter");
var splitLabel = document.getElementById("splitLabel");
var tc = document.getElementById("tc");
var rack = document.getElementById("rack");
var copyBtn = document.getElementById("copyBtn");
var toastEl = document.getElementById("toast");
var rExp = document.getElementById("rExp");
var rCon = document.getElementById("rCon");
var rSat = document.getElementById("rSat");
var rTemp = document.getElementById("rTemp");
var rLut = document.getElementById("rLut");
var split = 50; // percent
var active = LOOKS[0];
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 1800);
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
// Update wipe position (0-100)
function setSplit(pct) {
split = clamp(pct, 0, 100);
frameAfter.style.clipPath = "inset(0 0 0 " + split + "%)";
handle.style.left = split + "%";
splitLabel.textContent = Math.round(split) + "%";
viewer.setAttribute("aria-valuenow", String(Math.round(split)));
viewer.setAttribute("aria-valuetext", Math.round(split) + " percent graded");
}
// Apply a look to the graded half
function applyLook(look, animate) {
active = look;
if (animate) {
frameAfter.classList.add("is-wiping");
setTimeout(function () { frameAfter.classList.remove("is-wiping"); }, 400);
}
frameAfter.style.filter = look.filter;
tint.style.background = look.tint;
tagAfter.textContent = "AFTER · " + look.name.toUpperCase();
rExp.textContent = look.exp;
rCon.textContent = look.con;
rSat.textContent = look.sat;
rTemp.textContent = look.temp;
rLut.textContent = look.lut;
var cards = rack.querySelectorAll(".look");
cards.forEach(function (c) {
var on = c.dataset.id === look.id;
c.classList.toggle("is-active", on);
c.setAttribute("aria-checked", on ? "true" : "false");
});
}
// Build the look rack
LOOKS.forEach(function (look) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "look";
btn.dataset.id = look.id;
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
btn.setAttribute("aria-label", look.name + " look, " + look.lut);
btn.innerHTML =
'<span class="look__check" aria-hidden="true">✓</span>' +
'<span class="look__swatch" style="background:' + look.swatch + '"></span>' +
'<span class="look__body">' +
'<span class="look__name">' + look.name + '</span>' +
'<span class="look__lut">' + look.lut + '</span>' +
'</span>';
btn.addEventListener("click", function () {
applyLook(look, true);
toast("Look applied · " + look.name);
});
rack.appendChild(btn);
});
// Pointer drag on viewer
function pctFromEvent(clientX) {
var rect = viewer.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
}
var dragging = false;
function onDown(e) {
dragging = true;
viewer.focus();
setSplit(pctFromEvent(e.clientX));
if (viewer.setPointerCapture && e.pointerId != null) {
try { viewer.setPointerCapture(e.pointerId); } catch (_) {}
}
e.preventDefault();
}
function onMove(e) {
if (!dragging) return;
setSplit(pctFromEvent(e.clientX));
}
function onUp() {
dragging = false;
}
viewer.addEventListener("pointerdown", onDown);
viewer.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
viewer.addEventListener("pointercancel", onUp);
// Keyboard control
viewer.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 10 : 3;
if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
setSplit(split - step);
e.preventDefault();
} else if (e.key === "ArrowRight" || e.key === "ArrowUp") {
setSplit(split + step);
e.preventDefault();
} else if (e.key === "Home") {
setSplit(0);
e.preventDefault();
} else if (e.key === "End") {
setSplit(100);
e.preventDefault();
} else if (e.key >= "1" && e.key <= String(LOOKS.length)) {
var idx = parseInt(e.key, 10) - 1;
applyLook(LOOKS[idx], true);
toast("Look applied · " + LOOKS[idx].name);
e.preventDefault();
}
});
// Copy LUT name
copyBtn.addEventListener("click", function () {
var name = active.lut;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(name).then(
function () { toast("Copied · " + name); },
function () { toast(name); }
);
} else {
toast(name);
}
});
// Live-ish timecode ticker (25 fps, playing feel)
var frames = 4 * 25 + 12; // start 00:00:04:12
function fmt(n) { return n < 10 ? "0" + n : "" + n; }
function tick() {
frames = (frames + 1) % (60 * 60 * 25);
var f = frames % 25;
var totalSec = Math.floor(frames / 25);
var s = totalSec % 60;
var m = Math.floor(totalSec / 60) % 60;
var h = Math.floor(totalSec / 3600);
tc.textContent = fmt(h) + ":" + fmt(m) + ":" + fmt(s) + ":" + fmt(f);
}
setInterval(tick, 1000 / 25);
// Init
setSplit(50);
applyLook(LOOKS[0], false);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Grade — Before / After</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@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="bay">
<header class="bay__head">
<div class="bay__brand">
<span class="dot" aria-hidden="true"></span>
<div>
<h1>Grade Room</h1>
<p>Before / After color comparator</p>
</div>
</div>
<div class="bay__meta">
<span class="chip chip--rec"><span class="rec" aria-hidden="true"></span> A-CAM · RAW</span>
<span class="chip">ProRes 4444</span>
<span class="chip">Rec.709</span>
</div>
</header>
<!-- Comparator -->
<section class="stage" aria-label="Before and after grade comparison">
<div class="bar bar--top" aria-hidden="true"></div>
<div
class="viewer"
id="viewer"
role="slider"
tabindex="0"
aria-label="Grade wipe position"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
aria-valuetext="50 percent graded"
>
<!-- Ungraded base -->
<div class="frame frame--before" id="frameBefore">
<div class="tint tint--before" aria-hidden="true"></div>
</div>
<!-- Graded clip -->
<div class="frame frame--after" id="frameAfter">
<div class="tint tint--after" id="tint" aria-hidden="true"></div>
</div>
<span class="tag tag--before">BEFORE · UNGRADED</span>
<span class="tag tag--after" id="tagAfter">AFTER · KODAK 2383</span>
<div class="handle" id="handle" aria-hidden="true">
<span class="handle__line"></span>
<span class="handle__grip">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/><path d="M9 6l6 6-6 6" opacity=".55"/></svg>
</span>
</div>
<div class="hud" aria-hidden="true">
<span class="hud__tc" id="tc">00:00:04:12</span>
<span class="hud__split">SPLIT <b id="splitLabel">50%</b></span>
</div>
</div>
<div class="bar bar--bottom" aria-hidden="true"></div>
</section>
<!-- Readout -->
<section class="readout" aria-label="Active grade parameters">
<div class="stat"><span>Exposure</span><b id="rExp">+0.0</b></div>
<div class="stat"><span>Contrast</span><b id="rCon">+0</b></div>
<div class="stat"><span>Saturation</span><b id="rSat">+0</b></div>
<div class="stat"><span>Temp</span><b id="rTemp">0K</b></div>
<div class="stat stat--lut">
<span>Active LUT</span>
<b id="rLut">Kodak_2383_D65.cube</b>
</div>
<button class="copy" id="copyBtn" type="button">Copy LUT name</button>
</section>
<!-- Look rack -->
<section class="rack" aria-label="Available looks">
<h2 class="rack__title">Looks</h2>
<div class="rack__grid" id="rack" role="radiogroup" aria-label="Select a look"></div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A colorist-style before/after viewer built for grading demos. The hero frame sits inside cinematic black letterbox bars and is split by a draggable amber handle: everything left of the line shows the raw ungraded plate, everything right shows the currently selected look. Drag the handle, click anywhere on the frame to jump the wipe, or focus it and nudge with the arrow keys — the split percentage, exposure delta, and a live timecode all update in JetBrains Mono.
Beneath the frame, a rack of look cards (Kodak 2383, Teal Noir, Golden Hour, Bleach Bypass, Day-for-Night) each carry a LUT name badge and a gradient swatch. Selecting a card recolors the graded half via CSS filters and a tint overlay, animates a quick wipe, and refreshes the exposure/contrast/saturation readout. A copy button lifts the active LUT name to the clipboard with a toast.
Everything is vanilla JS with no dependencies. Pointer, click, and keyboard interactions share one update path, the layout collapses gracefully under 520px, and controls expose ARIA slider semantics with visible focus rings for accessible, keyboard-first grading.