Interior — Before After
A warm editorial before/after comparison slider that reveals a designed room over its bare shell as you drag a thin brass divider. Switch between three room studies, snap to preset reveal points, and toggle the fully-designed view for a hands-free reveal — keyboard, pointer, and touch friendly with WCAG AA labelling throughout.
MCP
Code
:root {
--bg: #f6f2ec;
--paper: #fbf9f5;
--ink: #2c2620;
--ink-2: #5a5147;
--muted: #8a8175;
--clay: #b08968;
--clay-d: #8c6a4f;
--walnut: #5c4433;
--sage: #9caf88;
--line: rgba(44, 38, 32, 0.12);
--white: #fff;
--r-sm: 4px;
--r-md: 8px;
--r-lg: 14px;
--shadow: 0 24px 60px -28px rgba(44, 38, 32, 0.4);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background:
radial-gradient(1200px 600px at 82% -8%, rgba(176, 137, 104, 0.14), 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;
min-height: 100vh;
padding: clamp(20px, 5vw, 64px) 20px;
}
.page {
max-width: 860px;
margin: 0 auto;
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
padding-bottom: 8px;
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--clay-d);
}
.masthead h1 {
margin: 0;
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 500;
font-size: clamp(2.6rem, 8vw, 4.2rem);
line-height: 1.02;
letter-spacing: 0.005em;
color: var(--ink);
}
.lede {
margin: 16px auto 0;
max-width: 46ch;
color: var(--ink-2);
font-size: 1rem;
}
/* ---------- Room tabs ---------- */
.rooms {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin: 34px 0 22px;
padding-top: 22px;
border-top: 1px solid var(--line);
}
.room {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
padding: 10px 18px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
cursor: pointer;
text-align: left;
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.room__name {
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 1.15rem;
font-weight: 600;
color: var(--ink);
}
.room__meta {
font-size: 0.7rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.room:hover {
transform: translateY(-2px);
border-color: rgba(176, 137, 104, 0.5);
}
.room.is-active {
background: var(--walnut);
border-color: var(--walnut);
}
.room.is-active .room__name {
color: var(--paper);
}
.room.is-active .room__meta {
color: rgba(251, 249, 245, 0.65);
}
.room:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 3px;
}
/* ---------- Stage / compare ---------- */
.stage {
margin: 0;
}
.compare {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid var(--line);
cursor: ew-resize;
user-select: none;
touch-action: none;
background: var(--paper);
}
.compare:focus-visible {
outline: 3px solid var(--clay-d);
outline-offset: 3px;
}
.layer {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
/* AFTER = fully designed room, sits underneath */
.layer--after {
z-index: 1;
}
/* BEFORE = empty shell, clipped from the right by the divider */
.layer--before {
z-index: 2;
overflow: hidden;
clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0);
}
.layer__inner {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
/* Empty-shell overlay: cooler, desaturated wash + a faint grid to read as "bare" */
.layer__inner::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(rgba(230, 224, 214, 0.45), rgba(210, 202, 190, 0.5)),
repeating-linear-gradient(0deg, rgba(44, 38, 32, 0.05) 0 1px, transparent 1px 42px),
repeating-linear-gradient(90deg, rgba(44, 38, 32, 0.05) 0 1px, transparent 1px 42px);
mix-blend-mode: multiply;
}
.tag {
position: absolute;
top: 14px;
z-index: 4;
padding: 6px 12px;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
border-radius: 999px;
backdrop-filter: blur(6px);
transition: opacity 0.25s ease;
}
.tag--before {
left: 14px;
background: rgba(44, 38, 32, 0.72);
color: var(--paper);
}
.tag--after {
right: 14px;
background: rgba(176, 137, 104, 0.9);
color: var(--paper);
}
/* Divider + handle */
.divider {
position: absolute;
top: 0;
bottom: 0;
left: var(--pos, 50%);
width: 2px;
transform: translateX(-1px);
background: linear-gradient(var(--clay), var(--clay-d));
z-index: 5;
box-shadow: 0 0 0 1px rgba(251, 249, 245, 0.35);
}
.handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 46px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
border-radius: 50%;
border: 1.5px solid var(--paper);
background: linear-gradient(150deg, var(--clay), var(--clay-d));
color: var(--paper);
cursor: grab;
box-shadow: 0 8px 22px -8px rgba(44, 38, 32, 0.7);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.compare.is-dragging .handle {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.08);
}
.handle__arrow {
font-size: 1rem;
line-height: 1;
opacity: 0.9;
}
.handle__grip {
width: 2px;
height: 16px;
background: rgba(251, 249, 245, 0.7);
border-radius: 2px;
}
.readout {
position: absolute;
right: 14px;
bottom: 14px;
z-index: 4;
padding: 5px 11px;
font-size: 0.78rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--ink);
background: rgba(251, 249, 245, 0.86);
border: 1px solid var(--line);
border-radius: var(--r-sm);
backdrop-filter: blur(4px);
}
.caption {
display: flex;
gap: 10px;
align-items: baseline;
margin: 18px 2px 0;
color: var(--ink-2);
font-size: 0.95rem;
}
.caption__dot {
flex: none;
width: 7px;
height: 7px;
margin-top: 6px;
border-radius: 50%;
background: var(--sage);
}
/* ---------- Controls ---------- */
.controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
margin-top: 26px;
padding-top: 22px;
border-top: 1px solid var(--line);
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
padding: 8px 14px;
font-family: inherit;
font-size: 0.82rem;
font-weight: 500;
color: var(--ink-2);
background: var(--paper);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: all 0.16s ease;
}
.chip:hover {
color: var(--ink);
border-color: var(--clay);
transform: translateY(-1px);
}
.chip.is-active {
color: var(--paper);
background: var(--clay-d);
border-color: var(--clay-d);
}
.chip:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 2px;
}
.play {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 9px 18px;
font-family: inherit;
font-size: 0.85rem;
font-weight: 600;
color: var(--paper);
background: var(--walnut);
border: 1px solid var(--walnut);
border-radius: 999px;
cursor: pointer;
transition: background 0.16s ease, transform 0.16s ease;
}
.play:hover {
background: #4a3728;
transform: translateY(-1px);
}
.play:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 2px;
}
.play__icon {
font-size: 0.7rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
padding: 11px 20px;
background: var(--ink);
color: var(--paper);
font-size: 0.85rem;
font-weight: 500;
border-radius: 999px;
box-shadow: 0 14px 34px -12px rgba(44, 38, 32, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 40;
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 24px 14px 40px;
}
.compare {
aspect-ratio: 4 / 5;
cursor: default;
}
.rooms {
gap: 8px;
}
.room {
flex: 1 1 44%;
padding: 9px 12px;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.presets {
justify-content: center;
}
.play {
justify-content: center;
}
.tag {
top: 10px;
padding: 5px 9px;
font-size: 0.6rem;
}
.tag--before {
left: 10px;
}
.tag--after {
right: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
var UNSPLASH = "https://images.unsplash.com/";
var Q = "?auto=format&fit=crop&w=1200&q=70";
// Each room: a designed photo (after) and an empty-shell photo (before).
// The empty layer also gets a desaturating wash via CSS so it reads as "bare".
var ROOMS = {
living: {
after: "photo-1567767292278-a4f21aa2d36e" + Q,
before: "photo-1505691938895-1758d7feb511" + Q,
pos: 50,
caption:
"Warm oak, boucle seating and a layered rug turn the bare loft into a reading room."
},
kitchen: {
after: "photo-1556911220-bff31c812dba" + Q,
before: "photo-1600585152220-90363fe7e115" + Q,
pos: 42,
caption:
"Clay-toned cabinetry and brass fittings replace the empty stud walls of Fenwick Terrace."
},
bedroom: {
after: "photo-1522708323590-d24dbb6b0267" + Q,
before: "photo-1560448204-e02f11c3d0e2" + Q,
pos: 58,
caption:
"Linen layers, a walnut headboard and soft sconces dress the stripped-back guest room."
}
};
var compare = document.getElementById("compare");
var beforeLayer = document.getElementById("beforeLayer");
var beforeInner = document.getElementById("beforeInner");
var afterLayer = document.getElementById("afterLayer");
var readout = document.getElementById("readout");
var caption = document.getElementById("caption");
var toastEl = document.getElementById("toast");
var playBtn = document.getElementById("play");
var playIcon = document.getElementById("playIcon");
var playLabel = document.getElementById("playLabel");
var roomBtns = Array.prototype.slice.call(document.querySelectorAll(".room"));
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var pos = 50;
var currentRoom = "living";
var playing = false;
var rafId = null;
var toastTimer = null;
function clamp(n) {
return Math.max(0, Math.min(100, n));
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 1900);
}
function render() {
var v = clamp(pos);
compare.style.setProperty("--pos", v + "%");
readout.textContent = Math.round(v) + "%";
compare.setAttribute("aria-valuenow", String(Math.round(v)));
compare.setAttribute("aria-valuetext", Math.round(v) + "% empty shell shown");
syncChips(v);
}
function syncChips(v) {
chips.forEach(function (chip) {
var target = Number(chip.getAttribute("data-pos"));
chip.classList.toggle("is-active", Math.round(v) === target);
});
}
function setPos(v, announce) {
pos = clamp(v);
render();
if (announce) {
toast("Reveal set to " + Math.round(pos) + "%");
}
}
function loadRoom(key) {
var room = ROOMS[key];
if (!room) return;
currentRoom = key;
afterLayer.style.backgroundImage = "url(" + UNSPLASH + room.after + ")";
beforeInner.style.backgroundImage = "url(" + UNSPLASH + room.before + ")";
caption.textContent = room.caption;
roomBtns.forEach(function (btn) {
var active = btn.getAttribute("data-room") === key;
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
setPos(room.pos, false);
}
/* ---------- Pointer dragging ---------- */
function posFromEvent(clientX) {
var rect = compare.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
}
var dragging = false;
function onDown(e) {
stopPlay();
dragging = true;
compare.classList.add("is-dragging");
compare.setPointerCapture && e.pointerId != null && compare.setPointerCapture(e.pointerId);
setPos(posFromEvent(e.clientX), false);
e.preventDefault();
}
function onMove(e) {
if (!dragging) return;
setPos(posFromEvent(e.clientX), false);
}
function onUp() {
if (!dragging) return;
dragging = false;
compare.classList.remove("is-dragging");
}
if (window.PointerEvent) {
compare.addEventListener("pointerdown", onDown);
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
} else {
compare.addEventListener("mousedown", onDown);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
compare.addEventListener("touchstart", function (e) {
onDown({ clientX: e.touches[0].clientX, preventDefault: function () {} });
});
window.addEventListener("touchmove", function (e) {
if (dragging) {
e.preventDefault();
setPos(posFromEvent(e.touches[0].clientX), false);
}
}, { passive: false });
window.addEventListener("touchend", onUp);
}
/* ---------- Keyboard ---------- */
compare.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 10 : 1;
var handled = true;
switch (e.key) {
case "ArrowRight":
case "ArrowUp":
stopPlay();
setPos(pos + step, false);
break;
case "ArrowLeft":
case "ArrowDown":
stopPlay();
setPos(pos - step, false);
break;
case "Home":
stopPlay();
setPos(0, true);
break;
case "End":
stopPlay();
setPos(100, true);
break;
case "PageUp":
stopPlay();
setPos(pos + 25, false);
break;
case "PageDown":
stopPlay();
setPos(pos - 25, false);
break;
default:
handled = false;
}
if (handled) e.preventDefault();
});
/* ---------- Presets ---------- */
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
stopPlay();
var v = Number(chip.getAttribute("data-pos"));
setPos(v, false);
toast(chip.textContent.trim() + " · " + v + "%");
});
});
/* ---------- Room tabs ---------- */
roomBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var key = btn.getAttribute("data-room");
if (key === currentRoom) return;
stopPlay();
loadRoom(key);
toast(btn.querySelector(".room__name").textContent + " loaded");
});
});
/* ---------- Play / auto-sweep ---------- */
var dir = 1;
var last = 0;
function tick(now) {
if (!playing) return;
if (!last) last = now;
var dt = (now - last) / 1000;
last = now;
pos += dir * dt * 34; // percent per second
if (pos >= 100) {
pos = 100;
dir = -1;
} else if (pos <= 0) {
pos = 0;
dir = 1;
}
render();
rafId = requestAnimationFrame(tick);
}
function startPlay() {
if (playing) return;
playing = true;
last = 0;
dir = pos >= 100 ? -1 : 1;
playBtn.setAttribute("aria-pressed", "true");
playIcon.textContent = "❚❚";
playLabel.textContent = "Pause";
rafId = requestAnimationFrame(tick);
toast("Playing reveal");
}
function stopPlay() {
if (!playing) return;
playing = false;
cancelAnimationFrame(rafId);
playBtn.setAttribute("aria-pressed", "false");
playIcon.textContent = "▶";
playLabel.textContent = "Play reveal";
}
playBtn.addEventListener("click", function () {
if (playing) {
stopPlay();
} else {
startPlay();
}
});
/* ---------- Init ---------- */
loadRoom("living");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Interior — Before / After Reveal</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=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="eyebrow">Studio Marrow · Portfolio</p>
<h1>Before & After</h1>
<p class="lede">
Drag the brass divider to reveal how each empty shell becomes a finished room.
Every study keeps its own reveal point — pick a room, slide the seam, or let it play.
</p>
</header>
<nav class="rooms" aria-label="Choose a room study">
<button class="room is-active" type="button" data-room="living" aria-pressed="true">
<span class="room__name">Living Room</span>
<span class="room__meta">Ashgrove Loft</span>
</button>
<button class="room" type="button" data-room="kitchen" aria-pressed="false">
<span class="room__name">Kitchen</span>
<span class="room__meta">Fenwick Terrace</span>
</button>
<button class="room" type="button" data-room="bedroom" aria-pressed="false">
<span class="room__name">Bedroom</span>
<span class="room__meta">Juniper House</span>
</button>
</nav>
<figure class="stage">
<div
class="compare"
id="compare"
role="slider"
tabindex="0"
aria-label="Reveal amount of the designed room"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
aria-valuetext="50% designed"
>
<div class="layer layer--after" id="afterLayer" aria-hidden="true"></div>
<div class="layer layer--before" id="beforeLayer">
<div class="layer__inner" id="beforeInner" aria-hidden="true"></div>
</div>
<span class="tag tag--before">Empty shell</span>
<span class="tag tag--after">Designed</span>
<div class="divider" id="divider">
<button class="handle" id="handle" type="button" aria-hidden="true" tabindex="-1">
<span class="handle__arrow">‹</span>
<span class="handle__grip"></span>
<span class="handle__arrow">›</span>
</button>
</div>
<output class="readout" id="readout">50%</output>
</div>
<figcaption class="caption">
<span class="caption__dot" aria-hidden="true"></span>
<span id="caption">Warm oak, boucle seating and a layered rug turn the bare loft into a reading room.</span>
</figcaption>
</figure>
<div class="controls">
<div class="presets" role="group" aria-label="Reveal presets">
<button class="chip" type="button" data-pos="0">Empty</button>
<button class="chip" type="button" data-pos="33">Reveal ⅓</button>
<button class="chip" type="button" data-pos="50">Split</button>
<button class="chip" type="button" data-pos="67">Reveal ⅔</button>
<button class="chip" type="button" data-pos="100">Designed</button>
</div>
<button class="play" id="play" type="button" aria-pressed="false">
<span class="play__icon" id="playIcon">▶</span>
<span id="playLabel">Play reveal</span>
</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A tactile before/after reveal built for interior design portfolios. Each study stacks the finished room above the empty shell inside a single frame; a draggable brass divider clips the top layer so the two states meet along a crisp seam. Grab the handle with a pointer, drag with touch, or nudge it in one-percent steps with the arrow keys — the seam, the position readout, and the two corner labels all update in lockstep.
A row of room tabs swaps the comparison between a living room, a kitchen, and a bedroom without leaving the page, each with its own caption and reveal position. Preset chips jump the divider to reveal thirds of the design, and a play toggle sweeps the seam across the frame so visitors can watch the transformation hands-free. A small toast confirms each action.
The layout is generous and editorial: thin rules, muted natural tones, and a Cormorant Garamond display serif over Inter body copy. Everything is responsive down to about 360px, controls stay keyboard reachable with visible focus rings, and the reveal position is announced through aria attributes for assistive technology.