Photography — Before After
A gallery-dark retouch comparison widget with a draggable divider that wipes between a raw and edited frame. Grab the handle with mouse, touch, or keyboard to reveal how a grade transforms the shot. Uppercase RAW and EDITED chips fade in and out as you drag, a thumbnail strip swaps subjects, and a snap-to-center control resets the split cleanly.
MCP
Code
:root {
--ink: #0b0b0c;
--bg: #101012;
--surface: #17171b;
--surface-2: #1e1e23;
--paper: #f6f4ef;
--sand: #c9b79c;
--sand-d: #a8916f;
--muted: #9a9aa2;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
background:
radial-gradient(900px 500px at 80% -10%, rgba(201, 183, 156, 0.10), transparent 60%),
var(--bg);
color: var(--paper);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
padding: 48px 24px 72px;
}
.wrap {
max-width: 860px;
margin: 0 auto;
}
/* Masthead */
.masthead { margin-bottom: 28px; }
.eyebrow {
display: inline-block;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--sand);
margin-bottom: 12px;
}
.masthead h1 {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(34px, 6vw, 52px);
line-height: 1.02;
margin: 0 0 10px;
letter-spacing: -0.01em;
}
.lede {
margin: 0;
color: var(--muted);
max-width: 46ch;
font-size: 15px;
}
/* Stage card */
.stage-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: 0 30px 80px -40px rgba(0, 0, 0, 0.9);
}
.stage-top {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 4px 6px 16px;
}
.shot-title {
display: block;
font-family: "Fraunces", Georgia, serif;
font-size: 19px;
font-weight: 600;
}
.shot-sub {
display: block;
font-size: 12px;
color: var(--muted);
letter-spacing: 0.02em;
margin-top: 2px;
}
.readout {
display: flex;
align-items: baseline;
gap: 3px;
white-space: nowrap;
}
.readout-val {
font-family: "Fraunces", Georgia, serif;
font-size: 26px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--sand);
}
.readout-pct {
font-size: 15px;
font-weight: 600;
color: var(--sand-d);
}
.readout-lbl {
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
margin-left: 6px;
}
/* Compare stage */
.compare {
position: relative;
width: 100%;
aspect-ratio: 3 / 2;
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface-2);
user-select: none;
-webkit-user-select: none;
touch-action: none;
cursor: ew-resize;
outline: none;
}
.compare:focus-visible {
box-shadow: 0 0 0 3px rgba(201, 183, 156, 0.55);
}
.layer {
position: absolute;
inset: 0;
}
.layer img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
/* Before layer is clipped from the right by JS */
.layer-before {
clip-path: inset(0 50% 0 0);
will-change: clip-path;
}
.layer-before img {
filter: saturate(0.35) contrast(0.92) brightness(0.9);
}
/* Chips */
.chip {
position: absolute;
top: 14px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
padding: 6px 11px;
border-radius: 999px;
background: rgba(11, 11, 12, 0.62);
border: 1px solid var(--line-2);
color: var(--white);
backdrop-filter: blur(6px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.chip-before { left: 14px; }
.chip-after {
right: 14px;
background: rgba(201, 183, 156, 0.9);
color: var(--ink);
border-color: transparent;
}
/* Divider + handle */
.divider {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 2px;
background: var(--white);
transform: translateX(-1px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
will-change: left;
}
.handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 46px;
height: 46px;
border-radius: 50%;
border: 2px solid var(--white);
background: rgba(11, 11, 12, 0.55);
color: var(--white);
backdrop-filter: blur(6px);
display: grid;
place-items: center;
cursor: ew-resize;
transition: transform 0.14s ease, background 0.14s ease, box-shadow 0.14s ease;
box-shadow: 0 8px 24px -6px rgba(0, 0, 0, 0.7);
}
.handle:hover { background: rgba(201, 183, 156, 0.85); color: var(--ink); }
.handle:active { transform: translate(-50%, -50%) scale(0.94); }
.compare:focus-visible .handle,
.handle:focus-visible {
box-shadow: 0 0 0 4px rgba(201, 183, 156, 0.55);
outline: none;
}
.handle-arrows { display: grid; place-items: center; }
.compare.dragging .handle { transform: translate(-50%, -50%) scale(1.06); }
.compare.dragging { cursor: grabbing; }
/* Controls */
.stage-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 6px 4px;
flex-wrap: wrap;
}
.thumbs {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.thumb {
width: 72px;
height: 52px;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--line);
padding: 0;
cursor: pointer;
background: var(--surface-2);
position: relative;
transition: border-color 0.15s ease, transform 0.15s ease;
}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.thumb:hover { transform: translateY(-2px); border-color: var(--line-2); }
.thumb[aria-selected="true"] {
border-color: var(--sand);
box-shadow: 0 0 0 1px var(--sand);
}
.thumb:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 183, 156, 0.55);
}
.reset {
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--paper);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.reset:hover { background: var(--sand); color: var(--ink); border-color: transparent; }
.reset:active { transform: scale(0.97); }
.reset:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 183, 156, 0.55);
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--paper);
color: var(--ink);
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 20;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
@media (max-width: 520px) {
body { padding: 32px 16px 60px; }
.stage-card { padding: 12px; }
.stage-top { flex-direction: column; align-items: flex-start; gap: 8px; }
.readout { align-self: flex-start; }
.stage-controls { flex-direction: column; align-items: stretch; }
.reset { width: 100%; }
.thumb { width: 62px; height: 44px; }
.handle { width: 42px; height: 42px; }
}(function () {
"use strict";
// Fictional shoots. `after` = edited (color/graded), `before` = raw look
// generated from the same base via Unsplash tuning params.
var SHOOTS = [
{
id: "portrait",
title: "Golden Hour Portrait",
sub: "85mm · f/1.4 · warm skin grade",
base: "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=1400&q=80"
},
{
id: "landscape",
title: "Fjord at First Light",
sub: "24mm · f/8 · sky replacement + haze pull",
base: "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1400&q=80"
},
{
id: "product",
title: "Ceramic Still Life",
sub: "100mm macro · dodge & burn cleanup",
base: "https://images.unsplash.com/photo-1493106641515-6b5631de4bb9?auto=format&fit=crop&w=1400&q=80"
},
{
id: "street",
title: "Neon District",
sub: "35mm · teal & orange split-tone",
base: "https://images.unsplash.com/photo-1519501025264-65ba15a82390?auto=format&fit=crop&w=1400&q=80"
}
];
var compare = document.getElementById("compare");
var layerBefore = document.getElementById("layerBefore");
var divider = document.getElementById("divider");
var handle = document.getElementById("handle");
var imgBefore = document.getElementById("imgBefore");
var imgAfter = document.getElementById("imgAfter");
var chipBefore = document.getElementById("chipBefore");
var chipAfter = document.getElementById("chipAfter");
var readoutVal = document.getElementById("readoutVal");
var shotTitle = document.getElementById("shotTitle");
var shotSub = document.getElementById("shotSub");
var thumbs = document.getElementById("thumbs");
var reset = document.getElementById("reset");
var toastEl = document.getElementById("toast");
var pos = 50; // percent from left where the divider sits
var dragging = false;
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 1800);
}
function clamp(n, lo, hi) {
return Math.max(lo, Math.min(hi, n));
}
function render() {
var v = clamp(pos, 0, 100);
// Before layer keeps its LEFT portion visible: clip from the right.
layerBefore.style.clipPath = "inset(0 " + (100 - v) + "% 0 0)";
divider.style.left = v + "%";
var rounded = Math.round(v);
readoutVal.textContent = String(100 - rounded); // % of edited image showing
compare.setAttribute("aria-valuenow", String(100 - rounded));
// Fade chips based on how much of their side is showing.
chipBefore.style.opacity = v < 12 ? "0.25" : "1";
chipAfter.style.opacity = v > 88 ? "0.25" : "1";
}
function setPos(v, announce) {
pos = clamp(v, 0, 100);
render();
if (announce) toast(Math.round(100 - pos) + "% edited");
}
function posFromClientX(clientX) {
var rect = compare.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
}
// Pointer events unify mouse + touch + pen.
function onDown(e) {
dragging = true;
compare.classList.add("dragging");
compare.setPointerCapture && e.pointerId != null && compare.setPointerCapture(e.pointerId);
setPos(posFromClientX(e.clientX), false);
e.preventDefault();
}
function onMove(e) {
if (!dragging) return;
setPos(posFromClientX(e.clientX), false);
}
function onUp() {
if (!dragging) return;
dragging = false;
compare.classList.remove("dragging");
}
compare.addEventListener("pointerdown", onDown);
window.addEventListener("pointermove", onMove, { passive: true });
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
// Keyboard support on the slider.
compare.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 10 : 3;
if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
setPos(pos - step, false);
e.preventDefault();
} else if (e.key === "ArrowRight" || e.key === "ArrowUp") {
setPos(pos + step, false);
e.preventDefault();
} else if (e.key === "Home") {
setPos(0, true);
e.preventDefault();
} else if (e.key === "End") {
setPos(100, true);
e.preventDefault();
}
});
// Prevent the handle button click from also triggering a jump.
handle.addEventListener("click", function (e) {
e.preventDefault();
compare.focus();
});
reset.addEventListener("click", function () {
setPos(50, false);
toast("Divider centered");
});
// Build thumbnail strip and wire selection.
var current = 0;
function loadShoot(idx, announce) {
var s = SHOOTS[idx];
current = idx;
imgAfter.src = s.base;
imgAfter.alt = "Edited version — " + s.title;
// Raw look: desaturated + flatter tone via Unsplash params.
imgBefore.src = s.base + "&sat=-80&exp=-1&con=-15";
imgBefore.alt = "Raw version — " + s.title;
shotTitle.textContent = s.title;
shotSub.textContent = s.sub;
Array.prototype.forEach.call(thumbs.children, function (btn, i) {
btn.setAttribute("aria-selected", i === idx ? "true" : "false");
});
setPos(50, false);
if (announce) toast("Loaded — " + s.title);
}
SHOOTS.forEach(function (s, i) {
var btn = document.createElement("button");
btn.className = "thumb";
btn.type = "button";
btn.setAttribute("role", "tab");
btn.setAttribute("aria-selected", i === 0 ? "true" : "false");
btn.setAttribute("aria-label", "Show " + s.title);
var im = document.createElement("img");
im.src = s.base + "&w=180&q=60";
im.alt = "";
im.draggable = false;
btn.appendChild(im);
btn.addEventListener("click", function () { loadShoot(i, true); });
thumbs.appendChild(btn);
});
loadShoot(0, false);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Retouch — 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=Fraunces:opsz,[email protected],400;9..144,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="masthead">
<span class="eyebrow">Studio Nord — Retouch Lab</span>
<h1>Before / After</h1>
<p class="lede">Drag the divider to reveal the graded frame. Works with mouse, touch, and arrow keys.</p>
</header>
<section class="stage-card" aria-label="Image comparison">
<div class="stage-top">
<div class="shot-meta">
<span class="shot-title" id="shotTitle">Golden Hour Portrait</span>
<span class="shot-sub" id="shotSub">85mm · f/1.4 · warm skin grade</span>
</div>
<div class="readout" aria-live="polite">
<span class="readout-val" id="readoutVal">50</span><span class="readout-pct">%</span>
<span class="readout-lbl">edited</span>
</div>
</div>
<div
class="compare"
id="compare"
role="slider"
tabindex="0"
aria-label="Reveal edited image"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
>
<div class="layer layer-after">
<img id="imgAfter" src="https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=1400&q=80" alt="Edited version of the photo" draggable="false" />
<span class="chip chip-after" id="chipAfter">Edited</span>
</div>
<div class="layer layer-before" id="layerBefore">
<img id="imgBefore" src="https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=1400&q=80&sat=-100&exp=-1" alt="Raw unedited version of the photo" draggable="false" />
<span class="chip chip-before" id="chipBefore">Raw</span>
</div>
<div class="divider" id="divider" aria-hidden="true">
<button class="handle" id="handle" type="button" aria-label="Drag to compare">
<span class="handle-arrows">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M9 6 4 12l5 6M15 6l5 6-5 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</button>
</div>
</div>
<div class="stage-controls">
<div class="thumbs" id="thumbs" role="tablist" aria-label="Choose a shoot"></div>
<button class="reset" id="reset" type="button">Snap to center</button>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A full-bleed before/after comparison built for retouchers. Two stacked frames — the untouched RAW capture and the finished EDIT — sit under a draggable vertical divider. Dragging the round handle wipes the top image away to reveal the graded version beneath, so clients can see exactly what a color pass, dodge-and-burn, or sky replacement bought them. The split is driven by pointer events, so it responds identically to mouse and touch, and the handle is a real focusable control you can nudge with the arrow keys.
Thin uppercase RAW and EDITED chips anchor each corner and brighten as their side of the frame grows, giving instant feedback about which version you are looking at. A thumbnail strip below the stage swaps between several fictional shoots — a portrait, a landscape, and a product still — each with its own before and after treatment, while a live percentage readout and a snap-to-50 reset button keep the comparison precise.
The layout is dark, generous, and image-first: a Fraunces display serif for the title, Inter for everything else, and hairline borders that keep the chrome out of the way of the photographs. It collapses gracefully to a single-column layout under 520px, keeps the handle large enough to grab on touch screens, and preserves visible focus rings throughout.