Interior — Materials Finishes
A warm editorial materials and finishes selector for interior projects. Browse swatches grouped by woods, stones, textiles and metals, click any tile to inspect an enlarged sample with its trade name, finish spec, origin and price band, then assemble a curated selection tray. A live palette bar previews how your picks read together, so a whole scheme comes into focus at a glance before you specify a single order.
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 1px 2px rgba(44, 38, 32, 0.05), 0 12px 30px -18px rgba(44, 38, 32, 0.35);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: clamp(18px, 4vw, 52px);
}
h1, h2 { font-family: "Cormorant Garamond", Georgia, serif; font-weight: 600; }
.shell { max-width: 1120px; margin: 0 auto; }
/* Masthead */
.masthead { margin-bottom: 30px; }
.masthead__eyebrow {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--clay-d);
}
.masthead__title {
margin: 8px 0 6px;
font-size: clamp(38px, 6vw, 60px);
line-height: 1.02;
letter-spacing: -0.01em;
color: var(--ink);
}
.masthead__lede {
margin: 0;
max-width: 54ch;
color: var(--ink-2);
font-size: 15.5px;
}
/* Layout */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
gap: 28px;
align-items: start;
}
/* Board */
.board__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding-bottom: 16px;
margin-bottom: 20px;
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
font: inherit;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
background: transparent;
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 15px;
cursor: pointer;
transition: background .18s, color .18s, border-color .18s, transform .12s;
}
.chip:hover { border-color: var(--clay); color: var(--ink); }
.chip:active { transform: translateY(1px); }
.chip.is-active {
background: var(--walnut);
border-color: var(--walnut);
color: var(--paper);
}
.chip:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }
.board__count {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--muted);
white-space: nowrap;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
gap: 16px;
}
/* Swatch */
.swatch {
font: inherit;
text-align: left;
padding: 0;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
cursor: pointer;
display: flex;
flex-direction: column;
transition: transform .16s ease, box-shadow .2s ease, border-color .2s ease;
}
.swatch:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: rgba(176, 137, 104, 0.5); }
.swatch:active { transform: translateY(-1px); }
.swatch:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }
.swatch.is-selected { border-color: var(--walnut); box-shadow: 0 0 0 1px var(--walnut); }
.swatch__sample {
height: 92px;
position: relative;
}
.swatch__sample::after {
content: "";
position: absolute;
inset: 0;
box-shadow: inset 0 -1px 0 rgba(44, 38, 32, 0.14);
}
.swatch__body { padding: 10px 12px 13px; }
.swatch__fam {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--clay-d);
}
.swatch__name {
margin: 3px 0 0;
font-size: 13.5px;
font-weight: 600;
color: var(--ink);
line-height: 1.25;
}
.swatch.is-selected .swatch__body::after {
content: "In tray";
display: inline-block;
margin-top: 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--sage);
}
/* Detail panel */
.panel { display: flex; flex-direction: column; gap: 22px; position: sticky; top: 24px; }
.detail {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow);
}
.detail__empty {
padding: 40px 26px;
text-align: center;
color: var(--muted);
}
.detail__empty p { margin: 14px 0 0; font-size: 14px; }
.detail__emptyIcon {
width: 46px; height: 46px;
margin: 0 auto;
border-radius: var(--r-sm);
border: 2px dashed rgba(138, 129, 117, 0.6);
}
.detail__sample { height: 150px; position: relative; }
.detail__sample .tag {
position: absolute;
top: 12px; left: 12px;
background: rgba(251, 249, 245, 0.92);
color: var(--walnut);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 5px 10px;
border-radius: 999px;
}
.detail__inner { padding: 18px 20px 22px; }
.detail__name { margin: 0; font-size: 27px; line-height: 1.1; color: var(--ink); }
.detail__code { margin: 3px 0 16px; font-size: 12px; color: var(--muted); letter-spacing: 0.06em; }
.spec { list-style: none; margin: 0 0 18px; padding: 0; }
.spec li {
display: flex;
justify-content: space-between;
gap: 14px;
padding: 8px 0;
border-top: 1px solid var(--line);
font-size: 13px;
}
.spec li:last-child { border-bottom: 1px solid var(--line); }
.spec dt, .spec .k { color: var(--muted); }
.spec .v { color: var(--ink); font-weight: 600; text-align: right; }
.price-band {
display: inline-flex; gap: 3px; align-items: center;
}
.price-band span {
width: 8px; height: 8px; border-radius: 50%;
background: var(--line);
}
.price-band span.on { background: var(--clay); }
.detail__add {
width: 100%;
font: inherit;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: var(--r-md);
padding: 12px;
cursor: pointer;
color: var(--paper);
background: var(--walnut);
transition: background .18s, transform .12s;
}
.detail__add:hover { background: #4a3729; }
.detail__add:active { transform: translateY(1px); }
.detail__add:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }
.detail__add[disabled] { background: var(--muted); cursor: not-allowed; opacity: .7; }
/* Tray */
.tray {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px 18px 20px;
box-shadow: var(--shadow);
}
.tray__head { display: flex; align-items: center; gap: 10px; }
.tray__title { margin: 0; font-size: 22px; color: var(--ink); }
.tray__badge {
margin-left: auto;
min-width: 24px;
text-align: center;
font-size: 12px;
font-weight: 700;
color: var(--paper);
background: var(--clay);
border-radius: 999px;
padding: 2px 8px;
}
.palette {
display: flex;
height: 20px;
margin: 14px 0 12px;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--line);
background:
repeating-linear-gradient(45deg, rgba(44,38,32,.05) 0 6px, transparent 6px 12px);
}
.palette__seg { flex: 1; transition: flex .3s ease; }
.tray__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.tray__item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 8px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
animation: pop .22s ease;
}
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.tray__dot { width: 22px; height: 22px; border-radius: var(--r-sm); flex: none; box-shadow: inset 0 0 0 1px rgba(44,38,32,.12); }
.tray__meta { min-width: 0; }
.tray__nm { font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tray__fam { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.tray__rm {
margin-left: auto;
font: inherit;
font-size: 16px;
line-height: 1;
color: var(--muted);
background: transparent;
border: none;
cursor: pointer;
padding: 4px 6px;
border-radius: var(--r-sm);
transition: color .15s, background .15s;
}
.tray__rm:hover { color: var(--clay-d); background: rgba(176,137,104,.12); }
.tray__rm:focus-visible { outline: 2px solid var(--clay); outline-offset: 1px; }
.tray__hint { margin: 12px 2px 16px; font-size: 12.5px; color: var(--muted); }
.tray__actions { display: flex; gap: 8px; }
.btn {
flex: 1;
font: inherit;
font-size: 13px;
font-weight: 600;
padding: 10px;
border-radius: var(--r-md);
cursor: pointer;
transition: background .18s, color .18s, transform .12s, border-color .18s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }
.btn--ghost { background: transparent; border: 1px solid var(--line); color: var(--ink-2); }
.btn--ghost:hover { border-color: var(--clay); color: var(--ink); }
.btn--solid { background: var(--clay); border: 1px solid var(--clay); color: var(--paper); }
.btn--solid:hover { background: var(--clay-d); border-color: var(--clay-d); }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--paper);
font-size: 13px;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 12px 30px -12px rgba(0,0,0,.5);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
z-index: 40;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.panel { position: static; }
}
@media (max-width: 520px) {
body { padding: 16px; }
.masthead { margin-bottom: 22px; }
.grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
.board__bar { margin-bottom: 16px; }
.swatch__sample { height: 80px; }
.detail__sample { height: 128px; }
.tray__actions { flex-direction: column; }
}/* Materials & Finishes selector — vanilla JS, no dependencies */
(function () {
"use strict";
var MAX = 6;
// Each swatch: css = the rendered sample surface, color = flat tone for palette/tray dot.
var MATERIALS = [
{ id: "walnut-oiled", family: "wood", name: "Oiled American Walnut", code: "WD-101", finish: "Hard-wax oil, matte", origin: "Ohio, USA", use: "Cabinetry & flooring", price: 3, color: "#5c4433",
css: "linear-gradient(115deg,#6b4e39,#4c3626 60%,#5c4433),repeating-linear-gradient(92deg,rgba(0,0,0,.12) 0 2px,transparent 2px 9px)" },
{ id: "white-oak", family: "wood", name: "Rift White Oak", code: "WD-118", finish: "UV lacquer, satin", origin: "Vermont, USA", use: "Joinery & panelling", price: 3, color: "#c9a878",
css: "linear-gradient(100deg,#d8bd97,#c2a074 55%,#cdae82),repeating-linear-gradient(90deg,rgba(120,86,52,.16) 0 1px,transparent 1px 7px)" },
{ id: "smoked-ash", family: "wood", name: "Fumed European Ash", code: "WD-124", finish: "Brushed, natural wax", origin: "Loire, FR", use: "Feature veneer", price: 4, color: "#7a6a58",
css: "linear-gradient(110deg,#8c7a64,#655645 60%,#7d6c58),repeating-linear-gradient(88deg,rgba(0,0,0,.14) 0 2px,transparent 2px 11px)" },
{ id: "travertine", family: "stone", name: "Roman Travertine", code: "ST-204", finish: "Honed, unfilled", origin: "Tivoli, IT", use: "Vanity & wall cladding", price: 4, color: "#d8c9ac",
css: "radial-gradient(circle at 30% 30%,#e7dcc4,#d3c3a3),repeating-linear-gradient(0deg,rgba(150,130,96,.18) 0 1px,transparent 1px 6px)" },
{ id: "calacatta", family: "stone", name: "Calacatta Viola", code: "ST-211", finish: "Polished slab", origin: "Carrara, IT", use: "Statement counters", price: 5, color: "#efe9e2",
css: "linear-gradient(135deg,#f5f1ea,#e9e2d8),linear-gradient(58deg,transparent 46%,rgba(140,90,80,.5) 47% 49%,transparent 50%),linear-gradient(24deg,transparent 62%,rgba(90,70,90,.35) 63% 64%,transparent 65%)" },
{ id: "basalt", family: "stone", name: "Flamed Basalt", code: "ST-227", finish: "Flamed, textured", origin: "Auvergne, FR", use: "Flooring & hearth", price: 3, color: "#4a4744",
css: "radial-gradient(circle at 60% 40%,#5a5652,#3c3936),repeating-radial-gradient(circle at 30% 70%,rgba(0,0,0,.25) 0 2px,transparent 2px 5px)" },
{ id: "boucle", family: "textile", name: "Ivory Wool Bouclé", code: "TX-302", finish: "Loop pile, 92k rubs", origin: "Yorkshire, UK", use: "Upholstery", price: 4, color: "#eae2d2",
css: "radial-gradient(circle at 20% 25%,#fff 0 2px,transparent 2px),radial-gradient(circle at 70% 60%,#fff 0 2px,transparent 2px),radial-gradient(circle at 45% 85%,#fff 0 1.5px,transparent 2px),#e6ddca" },
{ id: "linen", family: "textile", name: "Washed Belgian Linen", code: "TX-315", finish: "Stonewashed, plain", origin: "Kortrijk, BE", use: "Drapery & cushions", price: 2, color: "#c8b59a",
css: "repeating-linear-gradient(90deg,#cdba9d 0 2px,#c0ac8d 2px 4px),repeating-linear-gradient(0deg,rgba(120,100,70,.12) 0 2px,transparent 2px 4px)" },
{ id: "velvet", family: "textile", name: "Sage Cotton Velvet", code: "TX-329", finish: "Plush pile, matte", origin: "Como, IT", use: "Occasional seating", price: 4, color: "#9caf88",
css: "linear-gradient(115deg,#a8ba95,#8ba077 55%,#9caf88),repeating-linear-gradient(90deg,rgba(255,255,255,.12) 0 3px,transparent 3px 8px)" },
{ id: "brass", family: "metal", name: "Brushed Brass", code: "MT-401", finish: "Satin, lacquered", origin: "Sheffield, UK", use: "Hardware & trim", price: 3, color: "#b89a5e",
css: "linear-gradient(100deg,#d8bd7f,#a9894f 55%,#c7a86a),repeating-linear-gradient(100deg,rgba(255,255,255,.25) 0 1px,transparent 1px 5px)" },
{ id: "blackened-steel", family: "metal", name: "Blackened Steel", code: "MT-414", finish: "Waxed patina", origin: "Pittsburgh, USA", use: "Framing & shelving", price: 3, color: "#33312e",
css: "linear-gradient(120deg,#44413c,#2a2825 60%,#38352f),repeating-linear-gradient(120deg,rgba(255,255,255,.06) 0 1px,transparent 1px 6px)" },
{ id: "aged-copper", family: "metal", name: "Aged Copper", code: "MT-426", finish: "Living finish", origin: "Santa Fe, USA", use: "Accents & lighting", price: 4, color: "#9a6b4f",
css: "linear-gradient(120deg,#b07a57,#7e5238 55%,#a86e4d),radial-gradient(circle at 65% 35%,rgba(120,160,140,.4) 0 12%,transparent 40%)" }
];
var FAMILY_LABEL = { wood: "Wood", stone: "Stone", textile: "Textile", metal: "Metal" };
var grid = document.getElementById("grid");
var countEl = document.getElementById("count");
var detail = document.getElementById("detail");
var trayList = document.getElementById("trayList");
var trayCount = document.getElementById("trayCount");
var trayHint = document.getElementById("trayHint");
var palette = document.getElementById("palette");
var toastEl = document.getElementById("toast");
var selected = []; // array of material ids in tray
var activeId = null; // id shown in detail
var currentFilter = "all";
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 2000);
}
function byId(id) {
for (var i = 0; i < MATERIALS.length; i++) if (MATERIALS[i].id === id) return MATERIALS[i];
return null;
}
function priceBand(n) {
var out = '<span class="price-band" aria-label="Price band ' + n + ' of 5">';
for (var i = 1; i <= 5; i++) out += '<span class="' + (i <= n ? "on" : "") + '"></span>';
return out + "</span>";
}
/* ---- Grid ---- */
function renderGrid() {
grid.innerHTML = "";
var visible = 0;
MATERIALS.forEach(function (m) {
if (currentFilter !== "all" && m.family !== currentFilter) return;
visible++;
var btn = document.createElement("button");
btn.className = "swatch" + (selected.indexOf(m.id) !== -1 ? " is-selected" : "");
btn.type = "button";
btn.setAttribute("aria-pressed", activeId === m.id ? "true" : "false");
btn.setAttribute("data-id", m.id);
btn.innerHTML =
'<span class="swatch__sample" style="background:' + m.css + '"></span>' +
'<span class="swatch__body">' +
'<span class="swatch__fam">' + FAMILY_LABEL[m.family] + "</span>" +
'<span class="swatch__name">' + m.name + "</span>" +
"</span>";
btn.addEventListener("click", function () { showDetail(m.id); });
grid.appendChild(btn);
});
countEl.textContent = visible + (visible === 1 ? " finish" : " finishes");
}
/* ---- Detail ---- */
function showDetail(id) {
var m = byId(id);
if (!m) return;
activeId = id;
var inTray = selected.indexOf(id) !== -1;
detail.innerHTML =
'<div class="detail__sample" style="background:' + m.css + '">' +
'<span class="tag">' + FAMILY_LABEL[m.family] + "</span>" +
"</div>" +
'<div class="detail__inner">' +
'<h2 class="detail__name">' + m.name + "</h2>" +
'<p class="detail__code">' + m.code + "</p>" +
'<ul class="spec">' +
specRow("Finish", m.finish) +
specRow("Origin", m.origin) +
specRow("Application", m.use) +
'<li><span class="k">Price band</span><span class="v">' + priceBand(m.price) + "</span></li>" +
"</ul>" +
'<button class="detail__add" type="button" id="addBtn"' + (inTray ? " disabled" : "") + ">" +
(inTray ? "Already in tray" : "Add to selection tray") +
"</button>" +
"</div>";
var addBtn = document.getElementById("addBtn");
if (addBtn && !inTray) addBtn.addEventListener("click", function () { addToTray(id); });
// reflect pressed state in grid
var tiles = grid.querySelectorAll(".swatch");
tiles.forEach(function (t) {
t.setAttribute("aria-pressed", t.getAttribute("data-id") === id ? "true" : "false");
});
}
function specRow(k, v) {
return '<li><span class="k">' + k + '</span><span class="v">' + v + "</span></li>";
}
/* ---- Tray ---- */
function addToTray(id) {
if (selected.indexOf(id) !== -1) return;
if (selected.length >= MAX) { toast("Tray is full — remove one to add another"); return; }
selected.push(id);
var m = byId(id);
toast(m.name + " added to tray");
renderTray();
renderGrid();
if (activeId === id) showDetail(id); // refresh add button state
}
function removeFromTray(id) {
var i = selected.indexOf(id);
if (i === -1) return;
selected.splice(i, 1);
renderTray();
renderGrid();
if (activeId === id) showDetail(id);
toast("Removed from tray");
}
function renderTray() {
trayCount.textContent = String(selected.length);
trayList.innerHTML = "";
if (!selected.length) {
trayHint.textContent = "Nothing selected yet — add up to six materials to compose a scheme.";
} else {
trayHint.textContent = selected.length + " of " + MAX + " materials in scheme.";
}
selected.forEach(function (id) {
var m = byId(id);
var li = document.createElement("li");
li.className = "tray__item";
li.innerHTML =
'<span class="tray__dot" style="background:' + m.color + '"></span>' +
'<span class="tray__meta">' +
'<span class="tray__nm">' + m.name + "</span>" +
'<span class="tray__fam">' + FAMILY_LABEL[m.family] + " · " + m.code + "</span>" +
"</span>" +
'<button class="tray__rm" type="button" aria-label="Remove ' + m.name + '">×</button>';
li.querySelector(".tray__rm").addEventListener("click", function () { removeFromTray(id); });
trayList.appendChild(li);
});
renderPalette();
}
function renderPalette() {
palette.innerHTML = "";
if (!selected.length) return;
selected.forEach(function (id) {
var m = byId(id);
var seg = document.createElement("span");
seg.className = "palette__seg";
seg.style.background = m.color;
seg.title = m.name;
palette.appendChild(seg);
});
}
/* ---- Actions ---- */
document.getElementById("clearBtn").addEventListener("click", function () {
if (!selected.length) { toast("Tray is already empty"); return; }
selected = [];
renderTray();
renderGrid();
if (activeId) showDetail(activeId);
toast("Selection cleared");
});
document.getElementById("copyBtn").addEventListener("click", function () {
if (!selected.length) { toast("Add materials before copying"); return; }
var lines = ["Material & finishes schedule — Studio Larkspur", ""];
selected.forEach(function (id, i) {
var m = byId(id);
lines.push((i + 1) + ". " + m.name + " (" + m.code + ")");
lines.push(" " + FAMILY_LABEL[m.family] + " · " + m.finish + " · " + m.origin);
});
var text = lines.join("\n");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { toast("Scheme copied to clipboard"); },
function () { toast("Copy failed — select and copy manually"); }
);
} else {
toast("Clipboard unavailable in this view");
}
});
/* ---- Filters ---- */
var chips = document.querySelectorAll(".chip");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) { c.classList.remove("is-active"); c.setAttribute("aria-selected", "false"); });
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
currentFilter = chip.getAttribute("data-filter");
renderGrid();
});
});
/* ---- Init ---- */
renderGrid();
renderTray();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Materials & Finishes — Selector</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>
<main class="shell">
<header class="masthead">
<div class="masthead__eyebrow">Studio Larkspur · Specification Library</div>
<h1 class="masthead__title">Materials & Finishes</h1>
<p class="masthead__lede">Build a scheme the way you would at the sample table — browse by family, inspect a swatch, and collect what belongs together.</p>
</header>
<div class="layout">
<section class="board" aria-label="Materials board">
<div class="board__bar">
<div class="filters" role="tablist" aria-label="Filter by material family">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-filter="wood">Woods</button>
<button class="chip" role="tab" aria-selected="false" data-filter="stone">Stones</button>
<button class="chip" role="tab" aria-selected="false" data-filter="textile">Textiles</button>
<button class="chip" role="tab" aria-selected="false" data-filter="metal">Metals</button>
</div>
<span class="board__count" id="count" aria-live="polite">12 finishes</span>
</div>
<div class="grid" id="grid" aria-label="Swatch grid"><!-- swatches injected --></div>
</section>
<aside class="panel" aria-label="Selection and detail">
<section class="detail" id="detail" aria-live="polite">
<div class="detail__empty" id="detailEmpty">
<div class="detail__emptyIcon" aria-hidden="true"></div>
<p>Select a swatch to see its full specification.</p>
</div>
</section>
<section class="tray" aria-label="Selection tray">
<div class="tray__head">
<h2 class="tray__title">Selection tray</h2>
<span class="tray__badge" id="trayCount">0</span>
</div>
<div class="palette" id="palette" aria-label="Palette preview" title="Palette preview"></div>
<ul class="tray__list" id="trayList"></ul>
<p class="tray__hint" id="trayHint">Nothing selected yet — add up to six materials to compose a scheme.</p>
<div class="tray__actions">
<button class="btn btn--ghost" id="clearBtn" type="button">Clear</button>
<button class="btn btn--solid" id="copyBtn" type="button">Copy scheme</button>
</div>
</section>
</aside>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A materials library for the specification stage of an interior project, laid out with the calm of a physical sample board. Swatches are grouped into woods, stones, textiles and metals; each tile carries its own natural texture rendered in CSS so the grid reads like a tray of real samples. A category filter lets you narrow the wall to a single family, and the count of visible swatches updates as you go.
Clicking a swatch lifts it into a detail panel with an enlarged sample, its trade name, finish, origin, application note and a price band. From there you add it to a selection tray that holds your working scheme. Each tray chip can be removed, and a slim palette bar underneath blends every chosen material into a single preview strip so you can judge how walnut, travertine and brushed brass sit together.
The whole surface is keyboard-navigable — swatches are real buttons, the tray responds to focus, and a small toast confirms every add, remove or copy. Copying the scheme drops a tidy plain-text spec list onto the clipboard, ready to paste into a schedule or a note to the client.