Interior — Room Visualizer
A warm editorial room visualizer where a hand-styled living room repaints itself live as you pick wall, floor and accent palettes, swap material swatches on the sofa, rug and pillows, and dial the daylight — then save whole looks as chips you can recall or delete, with a running spec of the exact tones and finishes in play.
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;
/* live room tokens (JS-driven) */
--wall: #e9e2d6;
--wall-2: #ddd4c4;
--floor: #d8c3a0;
--floor-2: #c3a878;
--accent: #c47b56;
--sofa: #cbb79c;
--sofa-2: #b6a084;
--sofa-tex: repeating-radial-gradient(circle at 30% 30%, rgba(255,255,255,0.35) 0 2px, transparent 2px 5px);
--day: 0.42;
--daycolor: 255, 214, 160;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 700px at 80% -10%, #efe8dc 0%, transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: clamp(20px, 4vw, 52px) clamp(16px, 4vw, 40px) 64px;
}
/* ---------- masthead ---------- */
.masthead {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 16px 32px;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
margin-bottom: 30px;
}
.eyebrow {
margin: 0 0 6px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--clay-d);
}
h1 {
margin: 0;
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
font-size: clamp(34px, 6vw, 56px);
line-height: 1.02;
letter-spacing: 0.01em;
color: var(--walnut);
}
.lede {
margin: 0;
max-width: 34ch;
color: var(--ink-2);
font-size: 15px;
}
/* ---------- layout ---------- */
.studio {
display: grid;
grid-template-columns: 1fr 320px;
gap: 28px;
align-items: start;
}
/* ---------- stage ---------- */
.stage { min-width: 0; }
.room {
position: relative;
border-radius: var(--r-lg);
overflow: hidden;
aspect-ratio: 16 / 10;
box-shadow: 0 30px 60px -30px rgba(44, 38, 32, 0.5), 0 2px 0 rgba(255,255,255,0.6) inset;
border: 1px solid var(--line);
transition: filter 0.4s ease;
}
.wall {
position: absolute;
inset: 0 0 32% 0;
background:
linear-gradient(180deg, rgba(255,255,255,0.14), rgba(0,0,0,0.06)),
linear-gradient(180deg, var(--wall), var(--wall-2));
transition: background 0.5s ease;
}
/* daylight glow overlay on wall + floor */
.wall::after, .floor::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 90% at 78% 8%, rgba(var(--daycolor), var(--day)) 0%, transparent 62%);
pointer-events: none;
transition: background 0.4s ease, opacity 0.4s ease;
}
.window {
position: absolute;
top: 12%;
right: 8%;
width: 26%;
height: 62%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
padding: 7px;
border-radius: 4px;
background: var(--walnut);
box-shadow: 0 12px 24px -14px rgba(0,0,0,0.5);
}
.pane {
border-radius: 2px;
background:
linear-gradient(160deg, rgba(255,255,255,0.85), rgba(var(--daycolor), 0.65) 55%, #cfd8cf);
}
.artframe {
position: absolute;
top: 20%;
left: 12%;
width: 20%;
height: 46%;
margin: 0;
padding: 7px;
background: var(--paper);
border: 1px solid var(--line);
box-shadow: 0 14px 26px -16px rgba(0,0,0,0.45);
transform: rotate(-0.6deg);
}
.art {
height: 100%;
border-radius: 2px;
background:
linear-gradient(200deg, var(--accent) 0%, var(--sage) 100%),
var(--wall);
filter: saturate(0.85);
}
.sconce {
position: absolute;
top: 26%;
width: 10px;
height: 30px;
border-radius: 6px;
background: linear-gradient(180deg, var(--clay), var(--walnut));
box-shadow: 0 0 26px 8px rgba(var(--daycolor), calc(var(--day) * 0.9));
}
.sconce.left { left: 40%; }
.sconce.right { left: 52%; }
.floor {
position: absolute;
inset: 68% 0 0 0;
overflow: hidden;
background:
repeating-linear-gradient(90deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 46px),
linear-gradient(180deg, var(--floor) 0%, var(--floor-2) 100%);
transition: background 0.5s ease;
transform-style: preserve-3d;
}
.light {
position: absolute;
top: -4%;
right: 12%;
width: 46%;
height: 130%;
background: linear-gradient(180deg, rgba(var(--daycolor), calc(var(--day) * 0.85)), transparent 70%);
transform: skewX(-26deg);
filter: blur(6px);
transition: background 0.4s ease;
}
.rug {
position: absolute;
left: 50%;
bottom: 8%;
width: 72%;
height: 58%;
transform: translateX(-50%);
border-radius: 40% / 60%;
background:
radial-gradient(closest-side, rgba(255,255,255,0.16), transparent),
linear-gradient(180deg, color-mix(in srgb, var(--accent) 30%, var(--paper)), color-mix(in srgb, var(--accent) 14%, var(--paper)));
border: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
box-shadow: 0 18px 30px -18px rgba(0,0,0,0.5);
}
/* sofa */
.sofa {
position: absolute;
left: 50%;
bottom: 24%;
width: 52%;
height: 40%;
transform: translateX(-50%);
}
.sofa .back, .sofa .seat, .sofa .arm {
position: absolute;
background: linear-gradient(180deg, var(--sofa), var(--sofa-2));
box-shadow: 0 16px 26px -16px rgba(0,0,0,0.55), inset 0 2px 0 rgba(255,255,255,0.25);
transition: background 0.4s ease;
}
.sofa .back {
top: 0; left: 9%; right: 9%; height: 58%;
border-radius: 12px 12px 4px 4px;
background-image: var(--sofa-tex), linear-gradient(180deg, var(--sofa), var(--sofa-2));
background-blend-mode: soft-light, normal;
}
.sofa .seat {
bottom: 0; left: 6%; right: 6%; height: 46%;
border-radius: 8px;
}
.sofa .arm {
bottom: 0; width: 12%; height: 66%;
border-radius: 10px;
}
.sofa .arm.l { left: 0; }
.sofa .arm.r { right: 0; }
.sofa .pillow {
position: absolute;
top: 14%;
width: 15%;
height: 30%;
border-radius: 6px;
transform: rotate(-8deg);
box-shadow: 0 8px 14px -8px rgba(0,0,0,0.5);
transition: background 0.4s ease;
}
.sofa .pillow.p1 { left: 20%; background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000)); }
.sofa .pillow.p2 { right: 20%; transform: rotate(9deg); background: linear-gradient(180deg, var(--sage), color-mix(in srgb, var(--sage) 66%, #000)); }
.plant {
position: absolute;
left: 4%;
bottom: 20%;
width: 12%;
height: 44%;
}
.plant .pot {
position: absolute; bottom: 0; left: 20%; width: 60%; height: 34%;
border-radius: 4px 4px 8px 8px;
background: linear-gradient(180deg, var(--clay), var(--clay-d));
}
.plant .leaf {
position: absolute; bottom: 28%; left: 46%; width: 46%; height: 60%;
border-radius: 0 90% 0 90%;
background: linear-gradient(180deg, var(--sage), color-mix(in srgb, var(--sage) 60%, var(--walnut)));
transform-origin: bottom left;
}
.plant .leaf.a { transform: rotate(-24deg) scaleX(0.7); }
.plant .leaf.b { transform: rotate(4deg); }
.plant .leaf.c { transform: rotate(30deg) scaleX(0.7); }
.table {
position: absolute;
right: 6%;
bottom: 16%;
width: 15%;
height: 22%;
}
.table .top {
position: absolute; top: 0; left: 0; right: 0; height: 26%;
border-radius: 20px;
background: linear-gradient(180deg, var(--floor), var(--floor-2));
box-shadow: 0 10px 18px -12px rgba(0,0,0,0.5);
}
.table .leg {
position: absolute; bottom: 0; left: 40%; width: 20%; height: 80%;
background: var(--walnut);
border-radius: 2px;
}
/* daylight scene tone via data-attr fallback */
.room[data-daylight="cool"] { filter: saturate(0.94) brightness(0.99); }
.room[data-daylight="golden"] { filter: saturate(1.06) brightness(1.02); }
/* ---------- saved looks ---------- */
.saved { margin-top: 22px; }
.saved-head {
display: flex; align-items: center; justify-content: space-between;
gap: 14px; flex-wrap: wrap;
margin-bottom: 12px;
}
.saved-head h2 {
margin: 0;
font-family: "Cormorant Garamond", serif;
font-weight: 600;
font-size: 24px;
color: var(--walnut);
}
.saved-actions { display: flex; gap: 8px; }
.btn {
font: inherit;
font-weight: 600;
font-size: 13px;
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
border: 1px solid var(--line);
transition: transform 0.12s ease, background 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}
.btn:active { transform: translateY(1px); }
.btn.solid { background: var(--walnut); color: var(--paper); border-color: var(--walnut); }
.btn.solid:hover { background: var(--clay-d); box-shadow: 0 10px 20px -12px var(--clay-d); }
.btn.ghost { background: var(--paper); color: var(--ink); }
.btn.ghost:hover { background: #fff; box-shadow: 0 8px 18px -14px rgba(0,0,0,0.4); border-color: var(--clay); }
.btn:focus-visible, .swatch:focus-visible, .mat:focus-visible, .chip:focus-visible,
input[type="range"]:focus-visible, .chip-x:focus-visible {
outline: 2px solid var(--clay-d);
outline-offset: 2px;
}
.chips {
list-style: none;
margin: 0; padding: 0;
display: flex; flex-wrap: wrap; gap: 10px;
}
.chip {
position: relative;
display: inline-flex; align-items: center; gap: 9px;
padding: 7px 30px 7px 8px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
font: inherit; font-size: 12.5px; font-weight: 600;
color: var(--ink-2);
transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.chip:hover { transform: translateY(-1px); box-shadow: 0 10px 18px -14px rgba(0,0,0,0.4); border-color: var(--clay); }
.chip .dots { display: inline-flex; }
.chip .dots span {
width: 15px; height: 15px; border-radius: 50%;
border: 1.5px solid var(--paper);
margin-left: -5px;
box-shadow: 0 0 0 1px var(--line);
}
.chip .dots span:first-child { margin-left: 0; }
.chip-x {
position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
width: 20px; height: 20px; border-radius: 50%;
border: none; background: transparent; color: var(--muted);
cursor: pointer; font-size: 15px; line-height: 1;
display: grid; place-items: center;
transition: background 0.15s ease, color 0.15s ease;
}
.chip-x:hover { background: color-mix(in srgb, var(--clay) 25%, transparent); color: var(--walnut); }
.empty {
margin: 4px 0 0;
font-size: 13px;
color: var(--muted);
}
.empty strong { color: var(--ink-2); }
/* ---------- panel ---------- */
.panel {
position: sticky;
top: 20px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 6px 20px 20px;
box-shadow: 0 20px 44px -30px rgba(44, 38, 32, 0.45);
}
.group { padding: 18px 0; border-bottom: 1px solid var(--line); }
.group:last-child { border-bottom: 0; }
.group-head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 10px; margin-bottom: 12px;
}
.group-head h3 {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink);
}
.cur {
font-family: "Cormorant Garamond", serif;
font-size: 17px;
font-weight: 500;
color: var(--clay-d);
}
.swatches { display: flex; flex-wrap: wrap; gap: 9px; }
.swatch {
width: 38px; height: 38px;
border-radius: 50%;
border: 2px solid var(--paper);
box-shadow: 0 0 0 1px var(--line), 0 5px 10px -6px rgba(0,0,0,0.4);
cursor: pointer;
padding: 0;
position: relative;
transition: transform 0.14s ease, box-shadow 0.2s ease;
}
.swatch:hover { transform: translateY(-2px) scale(1.05); }
.swatch[aria-pressed="true"] {
box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--walnut), 0 6px 12px -6px rgba(0,0,0,0.4);
}
.swatch[aria-pressed="true"]::after {
content: "";
position: absolute; inset: 0;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.9) 0 4px, transparent 5px) center/100% no-repeat;
}
.materials { display: flex; flex-wrap: wrap; gap: 8px; }
.mat {
display: inline-flex; align-items: center; gap: 8px;
padding: 7px 12px 7px 8px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--white);
cursor: pointer;
font: inherit; font-size: 12.5px; font-weight: 600;
color: var(--ink-2);
transition: transform 0.12s ease, border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}
.mat .tex {
width: 20px; height: 20px; border-radius: 5px;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);
}
.mat:hover { transform: translateY(-1px); border-color: var(--clay); }
.mat[aria-pressed="true"] {
border-color: var(--walnut);
color: var(--walnut);
box-shadow: 0 0 0 1px var(--walnut);
}
input[type="range"] {
-webkit-appearance: none; appearance: none;
width: 100%; height: 6px; margin: 4px 0 6px;
border-radius: 999px;
background: linear-gradient(90deg, #9fb0c8, var(--paper) 45%, var(--clay));
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 20px; height: 20px; border-radius: 50%;
background: var(--paper);
border: 2px solid var(--walnut);
box-shadow: 0 3px 8px -3px rgba(0,0,0,0.5);
}
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px; border-radius: 50%;
background: var(--paper);
border: 2px solid var(--walnut);
}
.range-scale {
display: flex; justify-content: space-between;
font-size: 11px; color: var(--muted); letter-spacing: 0.04em;
}
.spec dl { margin: 0; display: grid; gap: 8px; }
.spec dl > div {
display: flex; align-items: center; justify-content: space-between;
gap: 10px;
padding-bottom: 8px;
border-bottom: 1px dotted var(--line);
}
.spec dl > div:last-child { border-bottom: 0; padding-bottom: 0; }
.spec dt {
font-size: 12px; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.1em;
}
.spec dd {
margin: 0;
font-size: 13px; font-weight: 600; color: var(--ink);
display: inline-flex; align-items: center; gap: 8px;
}
.spec dd .sw {
width: 15px; height: 15px; border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.12);
}
.spec dd .hex { font-size: 11px; color: var(--muted); font-weight: 500; }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%; bottom: 26px;
transform: translate(-50%, 20px);
background: var(--walnut);
color: var(--paper);
padding: 11px 20px;
border-radius: 999px;
font-size: 13.5px; font-weight: 600;
box-shadow: 0 16px 30px -14px rgba(0,0,0,0.6);
opacity: 0; pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* ---------- responsive ---------- */
@media (max-width: 900px) {
.studio { grid-template-columns: 1fr; }
.panel { position: static; }
}
@media (max-width: 520px) {
.wrap { padding: 20px 14px 48px; }
h1 { font-size: 36px; }
.masthead { margin-bottom: 22px; }
.room { aspect-ratio: 4 / 3; }
.studio { gap: 22px; }
.panel { padding: 4px 16px 16px; }
.swatch { width: 34px; height: 34px; }
.saved-actions { width: 100%; }
.saved-actions .btn { flex: 1; text-align: center; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; }
}(function () {
"use strict";
/* ---------- palette data ---------- */
var PALETTES = {
wall: [
{ name: "Plaster", hex: "#e9e2d6", hex2: "#ddd4c4" },
{ name: "Sage", hex: "#c6cdb4", hex2: "#b1ba9c" },
{ name: "Clay Wash", hex: "#e3c9b6", hex2: "#d3b39c" },
{ name: "Fog", hex: "#d6d4cf", hex2: "#c2c0ba" },
{ name: "Walnut Deep", hex: "#6f5540", hex2: "#5c4433" }
],
floor: [
{ name: "Pale Oak", hex: "#d8c3a0", hex2: "#c3a878" },
{ name: "Walnut", hex: "#8c6a4f", hex2: "#6f5038" },
{ name: "Ash", hex: "#cfc7bb", hex2: "#b3a99a" },
{ name: "Terrazzo", hex: "#e0d8cc", hex2: "#cabfae" },
{ name: "Ebony", hex: "#4a4038", hex2: "#332c26" }
],
accent: [
{ name: "Terracotta", hex: "#c47b56", hex2: "#a8613e" },
{ name: "Olive", hex: "#9caf88", hex2: "#7f9268" },
{ name: "Ochre", hex: "#cf9c4d", hex2: "#b3822f" },
{ name: "Rust", hex: "#a8543a", hex2: "#8a3f28" },
{ name: "Slate Blue", hex: "#7d94a6", hex2: "#607889" }
]
};
var MATERIALS = [
{
name: "Bouclé", swatch: "#cbb79c",
hex: "#cbb79c", hex2: "#b6a084",
tex: "repeating-radial-gradient(circle at 30% 30%, rgba(255,255,255,0.35) 0 2px, transparent 2px 5px)"
},
{
name: "Linen", swatch: "#d9cdb8",
hex: "#d9cdb8", hex2: "#c3b49b",
tex: "repeating-linear-gradient(45deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 4px)"
},
{
name: "Velvet", swatch: "#7d5c6d",
hex: "#7d5c6d", hex2: "#5f4351",
tex: "linear-gradient(180deg, rgba(255,255,255,0.18), rgba(0,0,0,0.15))"
},
{
name: "Leather", swatch: "#8a5a3c",
hex: "#8a5a3c", hex2: "#6c4429",
tex: "radial-gradient(circle at 40% 30%, rgba(255,255,255,0.16) 0 30%, transparent 60%)"
}
];
var DAY_STOPS = [
{ max: 25, name: "Overcast", rgb: "180, 200, 224", amt: 0.28, tag: "cool" },
{ max: 55, name: "Daylight", rgb: "236, 232, 214", amt: 0.36, tag: "golden" },
{ max: 80, name: "Golden", rgb: "255, 214, 160", amt: 0.5, tag: "golden" },
{ max: 101, name: "Sunset", rgb: "255, 176, 122", amt: 0.6, tag: "golden" }
];
/* ---------- state ---------- */
var state = {
wall: 0, floor: 0, accent: 0, material: 0, daylight: 70
};
var root = document.documentElement;
var saved = [];
/* ---------- helpers ---------- */
var $ = function (s, c) { return (c || document).querySelector(s); };
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 1900);
}
var liveEl = $("#live");
function announce(msg) { liveEl.textContent = msg; }
function copyHex(hex) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(hex).then(
function () { toast("Copied " + hex); },
function () { toast(hex); }
);
} else {
toast(hex);
}
}
function dayStop(v) {
for (var i = 0; i < DAY_STOPS.length; i++) {
if (v < DAY_STOPS[i].max) return DAY_STOPS[i];
}
return DAY_STOPS[DAY_STOPS.length - 1];
}
/* ---------- render ---------- */
function apply() {
var w = PALETTES.wall[state.wall];
var f = PALETTES.floor[state.floor];
var a = PALETTES.accent[state.accent];
var m = MATERIALS[state.material];
var d = dayStop(state.daylight);
root.style.setProperty("--wall", w.hex);
root.style.setProperty("--wall-2", w.hex2);
root.style.setProperty("--floor", f.hex);
root.style.setProperty("--floor-2", f.hex2);
root.style.setProperty("--accent", a.hex);
root.style.setProperty("--sofa", m.hex);
root.style.setProperty("--sofa-2", m.hex2);
root.style.setProperty("--sofa-tex", m.tex);
root.style.setProperty("--day", d.amt);
root.style.setProperty("--daycolor", d.rgb);
$("#room").setAttribute("data-daylight", d.tag);
// current labels
$("#cur-wall").textContent = w.name;
$("#cur-floor").textContent = f.name;
$("#cur-accent").textContent = a.name;
$("#cur-material").textContent = m.name;
$("#cur-light").textContent = d.name;
// spec
setSpec("wall", w.hex, w.name);
setSpec("floor", f.hex, f.name);
setSpec("accent", a.hex, a.name);
setSpec("material", m.hex, m.name);
// pressed states
updatePressed("wall", state.wall);
updatePressed("floor", state.floor);
updatePressed("accent", state.accent);
updateMatPressed();
}
function setSpec(key, hex, name) {
var dd = document.querySelector('[data-spec="' + key + '"]');
dd.innerHTML = "";
var sw = document.createElement("span");
sw.className = "sw";
sw.style.background = hex;
var label = document.createElement("span");
label.textContent = name;
var hx = document.createElement("span");
hx.className = "hex";
hx.textContent = hex.toUpperCase();
dd.appendChild(sw);
dd.appendChild(label);
dd.appendChild(hx);
}
function updatePressed(group, idx) {
var btns = document.querySelectorAll('#sw-' + group + ' .swatch');
for (var i = 0; i < btns.length; i++) {
btns[i].setAttribute("aria-pressed", i === idx ? "true" : "false");
}
}
function updateMatPressed() {
var btns = document.querySelectorAll('#sw-material .mat');
for (var i = 0; i < btns.length; i++) {
btns[i].setAttribute("aria-pressed", i === state.material ? "true" : "false");
}
}
/* ---------- build controls ---------- */
function buildSwatches(group) {
var host = $("#sw-" + group);
PALETTES[group].forEach(function (p, i) {
var b = document.createElement("button");
b.type = "button";
b.className = "swatch";
b.style.background = "linear-gradient(180deg, " + p.hex + ", " + p.hex2 + ")";
b.setAttribute("aria-pressed", "false");
b.setAttribute("aria-label", group + " " + p.name + " " + p.hex);
b.title = p.name + " · " + p.hex.toUpperCase() + " (click to copy)";
b.addEventListener("click", function () {
if (state[group] === i) {
copyHex(p.hex.toUpperCase());
return;
}
state[group] = i;
apply();
announce(group + " set to " + p.name);
});
host.appendChild(b);
});
}
function buildMaterials() {
var host = $("#sw-material");
MATERIALS.forEach(function (m, i) {
var b = document.createElement("button");
b.type = "button";
b.className = "mat";
b.setAttribute("aria-pressed", "false");
b.setAttribute("aria-label", "Upholstery " + m.name);
var tex = document.createElement("span");
tex.className = "tex";
tex.style.background = m.tex + ", linear-gradient(180deg," + m.hex + "," + m.hex2 + ")";
var t = document.createElement("span");
t.textContent = m.name;
b.appendChild(tex);
b.appendChild(t);
b.addEventListener("click", function () {
state.material = i;
apply();
announce("Upholstery set to " + m.name);
});
host.appendChild(b);
});
}
/* ---------- saved looks ---------- */
function schemeDots(s) {
return [
PALETTES.wall[s.wall].hex,
PALETTES.floor[s.floor].hex,
PALETTES.accent[s.accent].hex,
MATERIALS[s.material].swatch
];
}
function renderChips() {
var host = $("#chips");
host.innerHTML = "";
$("#empty").style.display = saved.length ? "none" : "";
saved.forEach(function (item) {
var li = document.createElement("li");
var chip = document.createElement("button");
chip.type = "button";
chip.className = "chip";
chip.setAttribute("aria-label", "Restore look " + item.label);
var dots = document.createElement("span");
dots.className = "dots";
schemeDots(item.scheme).forEach(function (c) {
var d = document.createElement("span");
d.style.background = c;
dots.appendChild(d);
});
var name = document.createElement("span");
name.textContent = item.label;
chip.appendChild(dots);
chip.appendChild(name);
chip.addEventListener("click", function () {
state.wall = item.scheme.wall;
state.floor = item.scheme.floor;
state.accent = item.scheme.accent;
state.material = item.scheme.material;
state.daylight = item.scheme.daylight;
$("#daylight").value = item.scheme.daylight;
apply();
toast("Restored “" + item.label + "”");
announce("Restored look " + item.label);
});
var x = document.createElement("button");
x.type = "button";
x.className = "chip-x";
x.innerHTML = "×";
x.setAttribute("aria-label", "Remove look " + item.label);
x.addEventListener("click", function (e) {
e.stopPropagation();
saved = saved.filter(function (s) { return s.id !== item.id; });
renderChips();
toast("Removed look");
});
li.appendChild(chip);
li.appendChild(x);
host.appendChild(li);
});
}
var seq = 0;
function saveLook() {
var label = PALETTES.wall[state.wall].name + " · " + PALETTES.accent[state.accent].name;
saved.unshift({
id: ++seq,
label: label,
scheme: {
wall: state.wall, floor: state.floor, accent: state.accent,
material: state.material, daylight: state.daylight
}
});
if (saved.length > 8) saved.pop();
renderChips();
toast("Saved “" + label + "”");
announce("Saved look " + label);
}
function shuffle() {
state.wall = Math.floor(Math.random() * PALETTES.wall.length);
state.floor = Math.floor(Math.random() * PALETTES.floor.length);
state.accent = Math.floor(Math.random() * PALETTES.accent.length);
state.material = Math.floor(Math.random() * MATERIALS.length);
state.daylight = 30 + Math.floor(Math.random() * 65);
$("#daylight").value = state.daylight;
apply();
toast("Fresh scheme rolled");
announce("Random scheme applied");
}
/* ---------- init ---------- */
buildSwatches("wall");
buildSwatches("floor");
buildSwatches("accent");
buildMaterials();
$("#daylight").addEventListener("input", function (e) {
state.daylight = parseInt(e.target.value, 10);
apply();
});
$("#save").addEventListener("click", saveLook);
$("#shuffle").addEventListener("click", shuffle);
// spec hex click-to-copy
$("#spec").addEventListener("click", function (e) {
var dd = e.target.closest("dd");
if (!dd) return;
var hx = dd.querySelector(".hex");
if (hx) copyHex(hx.textContent);
});
// seed a couple of tasteful saved looks
saved = [
{ id: ++seq, label: "Sage · Olive", scheme: { wall: 1, floor: 0, accent: 1, material: 0, daylight: 60 } },
{ id: ++seq, label: "Clay Wash · Terracotta", scheme: { wall: 2, floor: 1, accent: 0, material: 3, daylight: 78 } }
];
apply();
renderChips();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Interior — Room Visualizer</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=Cormorant+Garamond:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="wrap">
<header class="masthead">
<div>
<p class="eyebrow">Atelier Marlowe · Interiors</p>
<h1>Room Visualizer</h1>
</div>
<p class="lede">Pick palettes, swap materials and dial the daylight — the room repaints live. Save the looks you love.</p>
</header>
<main class="studio">
<!-- ROOM STAGE -->
<section class="stage" aria-label="Room preview">
<div class="room" id="room" data-daylight="warm">
<div class="wall" id="wall">
<div class="window" aria-hidden="true">
<span class="pane"></span><span class="pane"></span>
</div>
<figure class="artframe" aria-hidden="true">
<div class="art"></div>
</figure>
<div class="sconce left" aria-hidden="true"></div>
<div class="sconce right" aria-hidden="true"></div>
</div>
<div class="floor" id="floor">
<div class="light" id="light" aria-hidden="true"></div>
<div class="rug" id="rug" aria-hidden="true"></div>
<div class="sofa" id="sofa" aria-hidden="true">
<div class="back"></div>
<div class="seat"></div>
<div class="arm l"></div>
<div class="arm r"></div>
<div class="pillow p1"></div>
<div class="pillow p2"></div>
</div>
<div class="plant" aria-hidden="true"><span class="pot"></span><span class="leaf a"></span><span class="leaf b"></span><span class="leaf c"></span></div>
<div class="table" aria-hidden="true"><span class="top"></span><span class="leg"></span></div>
</div>
</div>
<div class="saved">
<div class="saved-head">
<h2>Saved looks</h2>
<div class="saved-actions">
<button type="button" class="btn ghost" id="shuffle">Shuffle</button>
<button type="button" class="btn solid" id="save">Save look</button>
</div>
</div>
<ul class="chips" id="chips" aria-label="Saved looks"></ul>
<p class="empty" id="empty">No saved looks yet — compose a room and press <strong>Save look</strong>.</p>
</div>
</section>
<!-- CONTROL COLUMN -->
<aside class="panel" aria-label="Room controls">
<div class="group" data-group="wall">
<div class="group-head"><h3>Wall</h3><span class="cur" id="cur-wall">Plaster</span></div>
<div class="swatches" id="sw-wall" role="group" aria-label="Wall palettes"></div>
</div>
<div class="group" data-group="floor">
<div class="group-head"><h3>Floor</h3><span class="cur" id="cur-floor">Pale Oak</span></div>
<div class="swatches" id="sw-floor" role="group" aria-label="Floor finishes"></div>
</div>
<div class="group" data-group="accent">
<div class="group-head"><h3>Accent</h3><span class="cur" id="cur-accent">Terracotta</span></div>
<div class="swatches" id="sw-accent" role="group" aria-label="Accent tones"></div>
</div>
<div class="group" data-group="material">
<div class="group-head"><h3>Upholstery</h3><span class="cur" id="cur-material">Bouclé</span></div>
<div class="materials" id="sw-material" role="group" aria-label="Sofa materials"></div>
</div>
<div class="group">
<div class="group-head"><h3>Daylight</h3><span class="cur" id="cur-light">Golden</span></div>
<input type="range" id="daylight" min="0" max="100" value="70" aria-label="Daylight warmth" />
<div class="range-scale"><span>Cool</span><span>Warm</span></div>
</div>
<div class="group spec">
<div class="group-head"><h3>Spec</h3></div>
<dl id="spec">
<div><dt>Wall</dt><dd data-spec="wall">—</dd></div>
<div><dt>Floor</dt><dd data-spec="floor">—</dd></div>
<div><dt>Accent</dt><dd data-spec="accent">—</dd></div>
<div><dt>Sofa</dt><dd data-spec="material">—</dd></div>
</dl>
</div>
</aside>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="sr-only" id="live" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The visualizer centers a pure-CSS living room — a back wall, a run of floorboards, a low sofa, a framed print, a rug and a scatter of pillows — rendered entirely with gradients so it repaints instantly. A curated column of controls sits alongside: named wall palettes (Plaster, Sage, Clay Wash), floor finishes (Pale Oak, Walnut, Ash) and accent tones, each a tappable swatch row, plus material chips that reskin the sofa in bouclé, linen or velvet and shuffle the pillow and rug fabrics to suit. A daylight slider warms or cools the whole scene, tinting the light spilling across the floor.
Every choice updates a live spec panel that lists the current wall, floor, accent and upholstery with their hex values, and any swatch copies its hex to the clipboard on click with a small toast. Press Save look and the current combination is captured as a labelled chip in the saved-looks rail; click a chip to restore that exact room, or use its remove control to discard it. A shuffle button rolls a tasteful random scheme for quick inspiration.
Controls are real buttons and inputs — keyboard reachable with visible focus rings, aria-pressed on the active swatch, and an aria-live region announcing changes. Under 520px the room stacks above a single scrollable control column, swatches wrap, and the editorial whitespace, thin walnut rules and muted natural palette carry through.