Construction — Before / After Slider
A draggable before and after image-comparison slider for renovation and trades work, with a grab handle, full arrow-key support, a toggle to show or hide the corner labels, and a gallery of project pairs below that swap into the main viewer on click.
MCP
Code
:root {
--hv: #f5c518; /* hi-vis yellow */
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6; /* concrete */
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a; /* safety orange */
--white: #ffffff;
--line: #d4d1cb;
--radius: 10px;
--shadow: 0 14px 34px rgba(27, 34, 43, 0.16);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background:
repeating-linear-gradient(135deg, transparent 0 28px, rgba(43, 52, 64, 0.02) 28px 30px),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
display: flex;
justify-content: center;
padding: 32px 18px;
}
.wrap {
width: 100%;
max-width: 680px;
}
/* ---------- Header ---------- */
.head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.tag {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--steel);
background: var(--hv);
padding: 3px 9px;
border-radius: 4px;
margin-bottom: 8px;
}
.head__title h1 {
font-size: 30px;
font-weight: 900;
letter-spacing: -0.02em;
text-transform: uppercase;
line-height: 1.02;
}
.head__title p {
font-size: 13px;
color: var(--muted);
margin-top: 4px;
}
.toggle {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 8px;
font-family: inherit;
font-size: 12px;
font-weight: 700;
color: var(--white);
background: var(--steel);
border: 2px solid var(--steel-d);
border-radius: 8px;
padding: 9px 12px;
cursor: pointer;
transition: background 0.15s ease, transform 0.08s ease;
}
.toggle:hover { background: var(--steel-d); }
.toggle:active { transform: translateY(1px); }
.toggle:focus-visible {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
.toggle__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--hv);
box-shadow: 0 0 0 3px rgba(245, 197, 24, 0.25);
transition: background 0.15s ease, box-shadow 0.15s ease;
}
.toggle[aria-pressed="false"] .toggle__dot {
background: var(--muted);
box-shadow: 0 0 0 3px rgba(102, 108, 117, 0.2);
}
/* ---------- Comparison viewer ---------- */
.ba {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
border-radius: var(--radius);
overflow: hidden;
border: 4px solid var(--steel);
box-shadow: var(--shadow);
user-select: none;
touch-action: none;
cursor: ew-resize;
}
.ba__layer {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
/* AFTER — clean, bright finish */
.ba__after {
background:
radial-gradient(circle at 70% 25%, rgba(255, 255, 255, 0.5), transparent 45%),
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.06) 0 38px, rgba(43, 52, 64, 0.05) 38px 40px),
linear-gradient(160deg, #f6efe2, #cdd6dd 60%, #a9b6c0);
}
/* BEFORE — dusty, grey, worn */
.ba__before {
background:
radial-gradient(circle at 30% 70%, rgba(0, 0, 0, 0.25), transparent 50%),
repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.08) 0 14px, transparent 14px 28px),
linear-gradient(160deg, #6f6a60, #4c4944 55%, #35332f);
/* clip-path is driven by JS via --pos */
clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0);
}
.ba__caption {
position: absolute;
bottom: 12px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 5px;
transition: opacity 0.2s ease;
}
.ba__caption--before {
left: 12px;
color: var(--white);
background: rgba(27, 34, 43, 0.78);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.ba__caption--after {
right: 12px;
color: var(--steel-d);
background: var(--hv);
}
.ba.labels-off .ba__caption {
opacity: 0;
pointer-events: none;
}
.ba__divider {
position: absolute;
top: 0;
bottom: 0;
left: var(--pos, 50%);
width: 3px;
background: var(--hv);
transform: translateX(-50%);
box-shadow: 0 0 0 1px rgba(27, 34, 43, 0.4);
pointer-events: none;
}
.ba__handle {
position: absolute;
top: 50%;
left: var(--pos, 50%);
transform: translate(-50%, -50%);
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--steel);
border: 3px solid var(--hv);
color: var(--hv);
font-size: 16px;
font-weight: 900;
cursor: ew-resize;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(27, 34, 43, 0.45);
transition: transform 0.08s ease, box-shadow 0.15s ease;
}
.ba__handle:hover {
box-shadow: 0 6px 16px rgba(27, 34, 43, 0.55);
}
.ba__handle:active {
transform: translate(-50%, -50%) scale(0.94);
}
.ba__handle:focus-visible {
outline: 4px solid var(--orange);
outline-offset: 3px;
}
.ba__arrows {
letter-spacing: -2px;
pointer-events: none;
}
.ba__readout {
position: absolute;
top: 12px;
left: 12px;
font-size: 12px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--white);
background: rgba(27, 34, 43, 0.78);
padding: 3px 8px;
border-radius: 5px;
pointer-events: none;
}
/* ---------- Active project meta ---------- */
.meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
background: var(--white);
border: 2px solid var(--line);
border-left: 5px solid var(--orange);
border-radius: var(--radius);
padding: 14px 16px;
margin-top: 16px;
}
.meta h2 {
font-size: 17px;
font-weight: 800;
letter-spacing: -0.01em;
}
.meta p {
font-size: 13px;
color: var(--muted);
margin-top: 2px;
}
.badge {
flex-shrink: 0;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #1c5b32;
background: #d6f0dd;
border: 1px solid #aed9bd;
padding: 4px 10px;
border-radius: 999px;
}
/* ---------- Gallery ---------- */
.gallery {
margin-top: 24px;
}
.gallery__h {
font-size: 13px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--steel);
margin-bottom: 10px;
}
.gallery__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.thumb {
position: relative;
aspect-ratio: 4 / 3;
border-radius: 8px;
overflow: hidden;
border: 3px solid var(--line);
background: var(--steel);
cursor: pointer;
padding: 0;
transition: border-color 0.15s ease, transform 0.08s ease;
}
.thumb::before,
.thumb::after {
content: "";
position: absolute;
inset: 0;
width: 50%;
}
.thumb::before {
left: 0;
background: linear-gradient(160deg, #6f6a60, #35332f);
}
.thumb::after {
right: 0;
left: auto;
background: var(--thumb-after, linear-gradient(160deg, #f6efe2, #a9b6c0));
}
.thumb__name {
position: absolute;
inset: auto 0 0 0;
z-index: 2;
font-size: 10px;
font-weight: 700;
color: var(--white);
background: rgba(27, 34, 43, 0.8);
padding: 4px 6px;
text-align: center;
}
.thumb:hover {
border-color: var(--steel);
transform: translateY(-2px);
}
.thumb:focus-visible {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
.thumb[aria-current="true"] {
border-color: var(--orange);
}
.thumb[aria-current="true"]::after {
box-shadow: inset 0 0 0 2px var(--hv);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body { padding: 20px 14px; }
.head {
flex-direction: column;
align-items: stretch;
}
.head__title h1 { font-size: 24px; }
.toggle { align-self: flex-start; }
.ba { aspect-ratio: 4 / 3; }
.meta {
flex-direction: column;
align-items: flex-start;
}
.gallery__grid {
grid-template-columns: repeat(2, 1fr);
}
}(function () {
"use strict";
var ba = document.getElementById("ba");
var beforeLayer = document.getElementById("beforeLayer");
var divider = document.getElementById("divider");
var handle = document.getElementById("handle");
var readout = document.getElementById("readout");
var labelToggle = document.getElementById("labelToggle");
var gallery = document.getElementById("gallery");
var projTitle = document.getElementById("projTitle");
var projDesc = document.getElementById("projDesc");
var projBadge = document.getElementById("projBadge");
if (!ba || !handle) return;
/* ---- Project data (CSS gradient placeholders for real photos) ---- */
var projects = [
{
name: "Kitchen",
title: "Kitchen Remodel",
desc: "Cabinet refit, quartz worktops & new splashback — 9 days.",
badge: "Completed",
before: "linear-gradient(160deg, #6f6a60, #4c4944 55%, #35332f)",
after: "linear-gradient(160deg, #f6efe2, #cdd6dd 60%, #a9b6c0)"
},
{
name: "Deck",
title: "Deck Rebuild",
desc: "Rotten boards stripped, new pressure-treated cedar — 4 days.",
badge: "Completed",
before: "linear-gradient(160deg, #5a4f42, #3a342b)",
after: "linear-gradient(160deg, #c79a5b, #8a5a2e 70%, #6b4322)"
},
{
name: "Driveway",
title: "Driveway Pour",
desc: "Cracked tarmac removed, fresh 100mm concrete slab — 3 days.",
badge: "Completed",
before: "linear-gradient(160deg, #4d4a45, #2c2a27)",
after: "linear-gradient(160deg, #d9d6cf, #aeaba3 65%, #8c8980)"
},
{
name: "Roof",
title: "Roof Replacement",
desc: "Stripped to battens, re-felted & re-tiled — 6 days.",
badge: "In progress",
before: "linear-gradient(160deg, #57514a, #322e2a)",
after: "linear-gradient(160deg, #8a3b2e, #5e2620 70%, #431915)"
}
];
var pos = 50; // current reveal percentage
var dragging = false;
var activeIndex = 0;
/* ---- Core: apply a position 0..100 ---- */
function setPos(value) {
pos = Math.max(0, Math.min(100, Math.round(value)));
var pct = pos + "%";
ba.style.setProperty("--pos", pct);
if (beforeLayer) beforeLayer.style.setProperty("--pos", pct);
ba.dataset.pos = String(pos);
handle.setAttribute("aria-valuenow", String(pos));
handle.setAttribute("aria-valuetext", pos + "% revealed");
if (readout) readout.textContent = pos + "%";
}
function posFromClientX(clientX) {
var rect = ba.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
}
/* ---- Pointer drag (mouse + touch via Pointer Events) ---- */
function startDrag(e) {
dragging = true;
if (ba.setPointerCapture && e.pointerId != null) {
try { ba.setPointerCapture(e.pointerId); } catch (err) {}
}
setPos(posFromClientX(e.clientX));
}
function moveDrag(e) {
if (!dragging) return;
e.preventDefault();
setPos(posFromClientX(e.clientX));
}
function endDrag() {
dragging = false;
}
ba.addEventListener("pointerdown", startDrag);
ba.addEventListener("pointermove", moveDrag);
ba.addEventListener("pointerup", endDrag);
ba.addEventListener("pointercancel", endDrag);
// Keep the handle from also firing a click-jump on pointerdown.
handle.addEventListener("pointerdown", function (e) { e.stopPropagation(); startDrag(e); });
/* ---- Keyboard support ---- */
handle.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 10 : 2;
switch (e.key) {
case "ArrowLeft":
case "ArrowDown":
e.preventDefault();
setPos(pos - step);
break;
case "ArrowRight":
case "ArrowUp":
e.preventDefault();
setPos(pos + step);
break;
case "Home":
e.preventDefault();
setPos(0);
break;
case "End":
e.preventDefault();
setPos(100);
break;
default:
break;
}
});
/* ---- Label toggle ---- */
if (labelToggle) {
labelToggle.addEventListener("click", function () {
var on = labelToggle.getAttribute("aria-pressed") === "true";
on = !on;
labelToggle.setAttribute("aria-pressed", String(on));
ba.classList.toggle("labels-off", !on);
labelToggle.querySelector(".toggle__txt").textContent = on ? "Labels on" : "Labels off";
});
}
/* ---- Build gallery + project switching ---- */
function loadProject(i) {
var p = projects[i];
if (!p) return;
activeIndex = i;
document.getElementById("afterLayer").style.background = p.after;
document.getElementById("beforeLayer").style.background =
"radial-gradient(circle at 30% 70%, rgba(0,0,0,0.25), transparent 50%)," +
"repeating-linear-gradient(45deg, rgba(0,0,0,0.08) 0 14px, transparent 14px 28px)," +
p.before;
if (projTitle) projTitle.textContent = p.title;
if (projDesc) projDesc.textContent = p.desc;
if (projBadge) {
projBadge.textContent = p.badge;
var inProgress = p.badge.toLowerCase().indexOf("progress") !== -1;
projBadge.style.color = inProgress ? "#8a4d12" : "#1c5b32";
projBadge.style.background = inProgress ? "#fbe6c8" : "#d6f0dd";
projBadge.style.borderColor = inProgress ? "#f0c98c" : "#aed9bd";
}
var thumbs = gallery.querySelectorAll(".thumb");
thumbs.forEach(function (t, idx) {
t.setAttribute("aria-current", idx === i ? "true" : "false");
});
setPos(50);
}
if (gallery) {
projects.forEach(function (p, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "thumb";
btn.style.setProperty("--thumb-after", p.after);
btn.setAttribute("aria-current", i === 0 ? "true" : "false");
btn.setAttribute("aria-label", "Show " + p.title);
var name = document.createElement("span");
name.className = "thumb__name";
name.textContent = p.name;
btn.appendChild(name);
btn.addEventListener("click", function () { loadProject(i); });
gallery.appendChild(btn);
});
}
/* ---- Init ---- */
loadProject(0);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Before / After Slider</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;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="head">
<div class="head__title">
<span class="tag">Brixton & Sons · Trades</span>
<h1>Project Showcase</h1>
<p>Drag the handle or use arrow keys to wipe between before and after.</p>
</div>
<button id="labelToggle" class="toggle" type="button" aria-pressed="true">
<span class="toggle__dot" aria-hidden="true"></span>
<span class="toggle__txt">Labels on</span>
</button>
</header>
<!-- Comparison viewer -->
<section
class="ba"
id="ba"
aria-label="Before and after image comparison"
data-pos="50"
>
<!-- AFTER is the base layer (full image) -->
<div class="ba__layer ba__after" id="afterLayer" aria-hidden="true">
<span class="ba__caption ba__caption--after">After</span>
</div>
<!-- BEFORE is clipped to the left of the handle -->
<div class="ba__layer ba__before" id="beforeLayer" aria-hidden="true">
<span class="ba__caption ba__caption--before">Before</span>
</div>
<div class="ba__divider" id="divider" aria-hidden="true"></div>
<button
class="ba__handle"
id="handle"
type="button"
role="slider"
aria-label="Reveal amount"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
aria-valuetext="50% revealed"
tabindex="0"
>
<span class="ba__arrows" aria-hidden="true">‹ ›</span>
</button>
<div class="ba__readout" id="readout" aria-live="polite">50%</div>
</section>
<!-- Active project meta -->
<div class="meta">
<div>
<h2 id="projTitle">Kitchen Remodel</h2>
<p id="projDesc">Cabinet refit, quartz worktops & new splashback — 9 days.</p>
</div>
<span class="badge" id="projBadge">Completed</span>
</div>
<!-- Gallery of pairs -->
<section class="gallery" aria-label="Other projects">
<h2 class="gallery__h">More projects</h2>
<div class="gallery__grid" id="gallery"><!-- buttons injected by JS --></div>
</section>
</main>
<script src="script.js"></script>
</body>
</html>Before / After Slider
A rugged image-comparison slider for showing off renovation and trades work. Drag the steel handle across to wipe between the Before and After states, or click anywhere on the frame to jump the divider there. The whole control is keyboard-driven: focus the handle and use the left/right arrows to nudge, Home/End to snap to either edge.
A hi-vis toggle shows or hides the corner labels for clean presentation screenshots, and a live percentage read-out tracks the split. Below the viewer sits a gallery of project pairs — kitchen remodel, deck rebuild, driveway pour and roof replacement — and clicking any thumbnail loads that pair into the main frame and resets the divider.
The “images” here are CSS gradients and texture placeholders so the snippet is fully self-contained;
swap them for real <img> photos in production. Everything is vanilla JS with pointer + keyboard
support and a responsive layout that collapses cleanly on phones.
Illustrative UI only — swap the placeholder gradients for real project photos before shipping.