Real Estate — Saved Searches / Alerts
An editorial saved-searches and alerts panel for a private-client real-estate brand. Buyers create named alerts from area, property type, minimum beds and a price ceiling, then tune each one to instant, daily, or weekly notice. Every saved row shows criteria chips, a faux listing photo, a live new-match badge, and quick controls to clear matches, mute, or delete. Vanilla JS handles creation, frequency toggles, badge clearing, and a small toast on save.
MCP
程式碼
:root {
--ivory: #f7f4ec;
--paper: #fffdf8;
--white: #ffffff;
--green: #1f3d34;
--green-d: #16302a;
--green-700: #26493e;
--green-50: #e8efea;
--brass: #b08d57;
--brass-d: #94733f;
--brass-50: #f3ead9;
--ink: #1c2a25;
--ink-2: #33433d;
--muted: #6b7a72;
--line: rgba(31, 61, 52, 0.12);
--line-2: rgba(31, 61, 52, 0.22);
--ok: #2f9e6f;
--warn: #c98a2b;
--danger: #c4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(28, 42, 37, 0.05), 0 2px 8px rgba(28, 42, 37, 0.05);
--sh-md: 0 6px 18px rgba(28, 42, 37, 0.08), 0 1px 3px rgba(28, 42, 37, 0.06);
--sh-lg: 0 18px 50px rgba(22, 48, 42, 0.14), 0 4px 14px rgba(22, 48, 42, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(120% 80% at 100% 0%, rgba(176, 141, 87, 0.07), transparent 55%),
radial-gradient(100% 70% at 0% 100%, rgba(31, 61, 52, 0.06), transparent 50%),
var(--ivory);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
letter-spacing: 0.2px;
margin: 0;
}
.shell {
max-width: 1040px;
margin: 0 auto;
padding: clamp(28px, 5vw, 56px) clamp(18px, 4vw, 40px) 72px;
}
/* ---------- Masthead ---------- */
.masthead { margin-bottom: 30px; }
.brand {
display: flex;
align-items: center;
gap: 16px;
}
.brand__mark {
flex: none;
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: var(--r-md);
background: linear-gradient(150deg, var(--green-700), var(--green-d));
color: var(--brass-50);
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 19px;
letter-spacing: 0.5px;
box-shadow: var(--sh-md);
border: 1px solid rgba(176, 141, 87, 0.35);
}
.brand__kicker {
margin: 0 0 1px;
font-size: 11.5px;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--brass-d);
font-weight: 600;
}
.brand__title {
font-size: clamp(30px, 5vw, 44px);
color: var(--green-d);
line-height: 1.04;
}
.masthead__lede {
margin: 16px 0 0;
max-width: 62ch;
color: var(--ink-2);
font-size: 15.5px;
}
/* ---------- Panels ---------- */
.panel {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
padding: clamp(18px, 3vw, 26px);
margin-bottom: 22px;
}
.panel__title {
font-size: 25px;
color: var(--green-d);
display: flex;
align-items: baseline;
gap: 10px;
}
.panel__count {
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 600;
color: var(--brass-d);
background: var(--brass-50);
border: 1px solid rgba(176, 141, 87, 0.35);
border-radius: 999px;
padding: 2px 9px;
line-height: 1.5;
}
.hairline {
display: block;
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--brass), transparent);
margin-left: 16px;
align-self: center;
}
/* ---------- Create form ---------- */
.create__head {
display: flex;
align-items: center;
margin-bottom: 18px;
}
.create__form {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 14px;
align-items: end;
}
.field { display: flex; flex-direction: column; gap: 6px; grid-column: span 4; }
.field--name { grid-column: span 6; }
.field--freq { grid-column: span 6; }
.field label,
.field__label {
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.6px;
text-transform: uppercase;
color: var(--muted);
}
.field input,
.field select {
font-family: inherit;
font-size: 14.5px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 11px 12px;
width: 100%;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field select {
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--green-700) 50%),
linear-gradient(135deg, var(--green-700) 50%, transparent 50%);
background-position: calc(100% - 17px) 19px, calc(100% - 12px) 19px;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 32px;
cursor: pointer;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: var(--green-700);
box-shadow: 0 0 0 3px rgba(31, 61, 52, 0.12);
}
.field input::placeholder { color: #9aa8a1; }
/* Segmented control */
.seg {
display: inline-flex;
background: var(--green-50);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 3px;
gap: 2px;
}
.seg__btn {
font-family: inherit;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: none;
border-radius: 6px;
padding: 7px 14px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.seg__btn:hover { color: var(--green-d); }
.seg__btn.is-active {
background: var(--white);
color: var(--green-d);
box-shadow: var(--sh-sm);
}
.seg__btn:focus-visible {
outline: 2px solid var(--brass);
outline-offset: 1px;
}
.seg--sm .seg__btn { padding: 5px 10px; font-size: 11.5px; }
/* Buttons */
.btn {
font-family: inherit;
font-weight: 600;
font-size: 14px;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 12px 20px;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.btn:active { transform: translateY(1px); }
.btn--primary {
background: linear-gradient(160deg, var(--green-700), var(--green-d));
color: var(--brass-50);
box-shadow: var(--sh-md);
border-color: rgba(176, 141, 87, 0.3);
}
.btn--primary:hover { box-shadow: var(--sh-lg); }
.btn--primary:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.create__submit { grid-column: span 6; justify-self: end; align-self: end; }
/* ---------- List header ---------- */
.list__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.newtotal {
font-size: 12.5px;
font-weight: 600;
color: var(--ok);
background: rgba(47, 158, 111, 0.1);
border: 1px solid rgba(47, 158, 111, 0.25);
border-radius: 999px;
padding: 4px 12px;
}
.newtotal[data-zero="true"] {
color: var(--muted);
background: transparent;
border-color: var(--line);
}
/* ---------- Search rows ---------- */
.searches { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.search {
display: grid;
grid-template-columns: 120px 1fr auto;
gap: 18px;
align-items: stretch;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
box-shadow: var(--sh-sm);
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
animation: rise 0.32s ease both;
}
.search:hover {
box-shadow: var(--sh-md);
transform: translateY(-2px);
border-color: var(--line-2);
}
.search.is-muted { opacity: 0.62; }
.search.is-removing { animation: fall 0.28s ease forwards; }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fall { to { opacity: 0; transform: translateX(-14px) scale(0.98); } }
/* Faux property photo */
.search__thumb {
position: relative;
border-radius: 10px;
overflow: hidden;
min-height: 92px;
border: 1px solid rgba(176, 141, 87, 0.25);
}
.search__thumb::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(0deg, rgba(22, 48, 42, 0.32), transparent 55%);
}
.search__tag {
position: absolute;
left: 8px;
bottom: 7px;
z-index: 1;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
color: #fff;
background: rgba(22, 48, 42, 0.55);
border: 1px solid rgba(243, 234, 217, 0.4);
backdrop-filter: blur(2px);
border-radius: 5px;
padding: 2px 7px;
}
.thumb-a { background:
radial-gradient(80% 60% at 70% 20%, #f0d9b0, transparent 60%),
linear-gradient(150deg, #c79a63 0%, #8a6a3f 48%, #4a5a4a 100%); }
.thumb-b { background:
radial-gradient(70% 50% at 25% 25%, #cfe0d2, transparent 55%),
linear-gradient(160deg, #6c8a78 0%, #36584a 55%, #1f3d34 100%); }
.thumb-c { background:
radial-gradient(85% 60% at 80% 15%, #fbe6c2, transparent 60%),
linear-gradient(135deg, #b9925c 0%, #7c5a34 50%, #2c3a33 100%); }
.thumb-d { background:
radial-gradient(75% 55% at 30% 70%, #e6d2a8, transparent 55%),
linear-gradient(200deg, #8d9b86 0%, #4d6453 60%, #16302a 100%); }
.thumb-e { background:
radial-gradient(70% 60% at 60% 30%, #f4ddb6, transparent 60%),
linear-gradient(125deg, #a98a64 0%, #5d6b58 55%, #283a32 100%); }
/* Body */
.search__body { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.search__top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.search__name {
font-size: 21px;
color: var(--green-d);
line-height: 1.1;
}
.badge {
font-family: "Inter", sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3px;
border-radius: 999px;
padding: 3px 9px;
line-height: 1.4;
}
.badge--new {
color: #fff;
background: var(--danger);
box-shadow: 0 2px 6px rgba(196, 80, 62, 0.35);
animation: pop 0.3s ease both;
}
@keyframes pop { from { transform: scale(0.7); } to { transform: scale(1); } }
.search__chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
font-size: 12px;
font-weight: 500;
color: var(--ink-2);
background: var(--ivory);
border: 1px solid var(--line);
border-radius: 6px;
padding: 3px 9px;
}
.chip--price { color: var(--brass-d); background: var(--brass-50); border-color: rgba(176, 141, 87, 0.3); font-weight: 600; }
.chip--beds { color: var(--green-700); background: var(--green-50); border-color: rgba(31, 61, 52, 0.16); }
.search__sub { margin: 0; font-size: 12.5px; color: var(--muted); }
/* Controls */
.search__controls {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.search__actions { display: flex; gap: 6px; }
.icon-btn {
font-family: inherit;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: 6px;
padding: 6px 11px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.icon-btn:hover { background: var(--green-50); color: var(--green-d); border-color: var(--line-2); }
.icon-btn:focus-visible { outline: 2px solid var(--brass); outline-offset: 1px; }
.icon-btn[aria-pressed="true"] { background: var(--green-700); color: var(--brass-50); border-color: var(--green-700); }
.icon-btn--danger:hover { background: rgba(196, 80, 62, 0.1); color: var(--danger); border-color: rgba(196, 80, 62, 0.35); }
.empty {
text-align: center;
color: var(--muted);
font-size: 14px;
padding: 28px 12px;
margin: 0;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--green-d);
color: var(--brass-50);
font-size: 13.5px;
font-weight: 500;
padding: 12px 20px;
border-radius: var(--r-md);
box-shadow: var(--sh-lg);
border: 1px solid rgba(176, 141, 87, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
max-width: calc(100vw - 32px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.create__form { grid-template-columns: repeat(6, 1fr); }
.field, .field--name, .field--freq { grid-column: span 6; }
.field--type, .field--beds, .field--max { grid-column: span 3; }
.create__submit { grid-column: span 6; justify-self: stretch; }
.search { grid-template-columns: 96px 1fr; }
.search__controls {
grid-column: 1 / -1;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
padding-top: 4px;
border-top: 1px solid var(--line);
}
}
@media (max-width: 520px) {
.shell { padding: 24px 16px 56px; }
.brand { gap: 12px; }
.brand__mark { width: 48px; height: 48px; font-size: 16px; }
.panel { padding: 16px; border-radius: var(--r-md); }
.panel__title { font-size: 22px; }
.hairline { display: none; }
.field { grid-column: span 6 !important; }
.search { grid-template-columns: 1fr; }
.search__thumb { min-height: 120px; }
.search__controls { align-items: stretch; }
.seg--sm { width: 100%; justify-content: center; }
.seg--sm .seg__btn { flex: 1; }
.search__actions { width: 100%; }
.search__actions .icon-btn { flex: 1; }
.toast { left: 16px; right: 16px; transform: translate(0, 24px); max-width: none; }
.toast.is-show { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
var THUMBS = ["thumb-a", "thumb-b", "thumb-c", "thumb-d", "thumb-e"];
var TAGS = ["New build", "Waterfront", "Period home", "Garden", "City view"];
var seed = [
{
id: "s1",
name: "Cedar Hill — Family",
area: "Cedar Hill",
type: "House",
beds: "3+ beds",
price: "≤ $850k",
freq: "Instant",
muted: false,
newCount: 4,
thumb: 0,
tag: 0,
},
{
id: "s2",
name: "Harbor Point Lofts",
area: "Harbor Point",
type: "Loft",
beds: "2+ beds",
price: "≤ $1.2M",
freq: "Daily",
muted: false,
newCount: 1,
thumb: 1,
tag: 1,
},
{
id: "s3",
name: "Old Meridian Estate",
area: "Old Meridian",
type: "Estate",
beds: "4+ beds",
price: "≤ $3M",
freq: "Weekly",
muted: true,
newCount: 0,
thumb: 2,
tag: 2,
},
{
id: "s4",
name: "Ashbourne Townhouse",
area: "Ashbourne",
type: "Townhouse",
beds: "3+ beds",
price: "≤ $1.8M",
freq: "Daily",
muted: false,
newCount: 2,
thumb: 3,
tag: 3,
},
];
var listEl = document.getElementById("searches");
var tpl = document.getElementById("row-tpl");
var countEl = document.getElementById("count");
var emptyEl = document.getElementById("empty");
var newTotalEl = document.getElementById("newtotal");
var toastEl = document.getElementById("toast");
var form = document.getElementById("create-form");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
function setFreq(segEl, freq) {
var btns = segEl.querySelectorAll(".seg__btn");
btns.forEach(function (b) {
var active = b.dataset.freq === freq;
b.classList.toggle("is-active", active);
b.setAttribute("aria-checked", active ? "true" : "false");
});
}
function renderRow(data) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.dataset.id = data.id;
var thumb = node.querySelector(".search__thumb");
thumb.classList.add(THUMBS[data.thumb % THUMBS.length]);
node.querySelector(".search__tag").textContent = TAGS[data.tag % TAGS.length];
node.querySelector(".search__name").textContent = data.name;
var chips = node.querySelector(".search__chips");
var defs = [
{ t: data.area, c: "chip" },
{ t: data.type, c: "chip" },
{ t: data.beds, c: "chip chip--beds" },
{ t: data.price, c: "chip chip--price" },
];
defs.forEach(function (d) {
var s = document.createElement("span");
s.className = d.c;
s.textContent = d.t;
chips.appendChild(s);
});
var seg = node.querySelector(".seg");
setFreq(seg, data.freq);
node.querySelector(".search__sub").textContent = subText(data);
var muteBtn = node.querySelector('[data-act="mute"]');
applyMute(node, muteBtn, data.muted, true);
updateBadge(node, data.newCount);
listEl.appendChild(node);
}
function subText(data) {
if (data.muted) return "Alerts muted";
if (data.newCount > 0) {
return data.newCount + " new since last visit · " + data.freq.toLowerCase() + " alerts";
}
return "Up to date · " + data.freq.toLowerCase() + " alerts";
}
function refreshSub(node) {
var d = getData(node.dataset.id);
node.querySelector(".search__sub").textContent = subText(d);
}
function updateBadge(node, n) {
var badge = node.querySelector("[data-new]");
if (n > 0) {
badge.hidden = false;
badge.textContent = n + " new";
} else {
badge.hidden = true;
}
}
function applyMute(node, btn, muted, silent) {
node.classList.toggle("is-muted", muted);
btn.setAttribute("aria-pressed", muted ? "true" : "false");
btn.textContent = muted ? "Muted" : "Mute";
if (!silent) refreshSub(node);
}
function getData(id) {
return seed.filter(function (s) { return s.id === id; })[0];
}
function updateTotals() {
var visible = seed.length;
countEl.textContent = String(visible);
var totalNew = seed.reduce(function (acc, s) {
return acc + (s.muted ? 0 : s.newCount);
}, 0);
newTotalEl.textContent = totalNew + (totalNew === 1 ? " new match" : " new matches");
newTotalEl.setAttribute("data-zero", totalNew === 0 ? "true" : "false");
emptyEl.hidden = visible !== 0;
}
// ----- Row interactions (event delegation) -----
listEl.addEventListener("click", function (e) {
var freqBtn = e.target.closest(".seg__btn");
var actBtn = e.target.closest("[data-act]");
var row = e.target.closest(".search");
if (!row) return;
var data = getData(row.dataset.id);
if (!data) return;
if (freqBtn) {
data.freq = freqBtn.dataset.freq;
setFreq(row.querySelector(".seg"), data.freq);
refreshSub(row);
toast('"' + data.name + '" set to ' + data.freq.toLowerCase() + " alerts");
return;
}
if (actBtn) {
var act = actBtn.dataset.act;
if (act === "clear") {
if (data.newCount === 0) {
toast("No new matches to clear");
return;
}
data.newCount = 0;
updateBadge(row, 0);
refreshSub(row);
updateTotals();
toast("Cleared new matches for " + data.name);
} else if (act === "mute") {
data.muted = !data.muted;
applyMute(row, actBtn, data.muted, false);
updateTotals();
toast(data.muted ? data.name + " muted" : data.name + " unmuted");
} else if (act === "delete") {
row.classList.add("is-removing");
var id = data.id;
setTimeout(function () {
seed = seed.filter(function (s) { return s.id !== id; });
row.remove();
updateTotals();
}, 260);
toast("Deleted " + data.name);
}
}
});
// ----- Create-form frequency segmented control -----
var createSeg = form.querySelector(".seg");
var createFreq = "Instant";
createSeg.addEventListener("click", function (e) {
var btn = e.target.closest(".seg__btn");
if (!btn) return;
createFreq = btn.dataset.freq;
setFreq(createSeg, createFreq);
});
// ----- Create new alert -----
var idCounter = 100;
form.addEventListener("submit", function (e) {
e.preventDefault();
var nameInput = form.elements.name;
var name = nameInput.value.trim();
if (!name) {
nameInput.focus();
toast("Give your search a name first");
return;
}
var data = {
id: "s" + ++idCounter,
name: name,
area: form.elements.area.value,
type: form.elements.type.value,
beds: form.elements.beds.value,
price: form.elements.price.value,
freq: createFreq,
muted: false,
newCount: 0,
thumb: idCounter % THUMBS.length,
tag: idCounter % TAGS.length,
};
seed.unshift(data);
// render at top
var node = renderAtTop(data);
updateTotals();
nameInput.value = "";
createFreq = "Instant";
setFreq(createSeg, createFreq);
toast("Saved “" + name + "” — " + createFreqLabel(data.freq));
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
function createFreqLabel(freq) {
return freq.toLowerCase() + " alerts on";
}
function renderAtTop(data) {
renderRow(data);
var node = listEl.lastElementChild;
listEl.insertBefore(node, listEl.firstElementChild);
return node;
}
// ----- Initial render -----
seed.forEach(renderRow);
updateTotals();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Saved Searches & Alerts — Marrow & Vale</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@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<header class="masthead">
<div class="brand">
<span class="brand__mark" aria-hidden="true">M&V</span>
<div>
<p class="brand__kicker">Marrow & Vale · Private Client</p>
<h1 class="brand__title">Saved Searches & Alerts</h1>
</div>
</div>
<p class="masthead__lede">
Track the market on your terms. Tune each search to instant, daily, or weekly
notice — we'll surface new listings the moment they meet your brief.
</p>
</header>
<!-- Create alert -->
<section class="panel create" aria-labelledby="create-h">
<div class="create__head">
<h2 id="create-h" class="panel__title">Create a new alert</h2>
<span class="hairline" aria-hidden="true"></span>
</div>
<form id="create-form" class="create__form" novalidate>
<div class="field field--name">
<label for="f-name">Search name</label>
<input id="f-name" name="name" type="text" placeholder="e.g. Cedar Hill — 3 bed" autocomplete="off" required />
</div>
<div class="field">
<label for="f-area">Area</label>
<select id="f-area" name="area">
<option>Cedar Hill</option>
<option>Harbor Point</option>
<option>Old Meridian</option>
<option>Ashbourne</option>
<option>Westfen Quay</option>
</select>
</div>
<div class="field">
<label for="f-type">Property</label>
<select id="f-type" name="type">
<option>House</option>
<option>Townhouse</option>
<option>Condo</option>
<option>Loft</option>
<option>Estate</option>
</select>
</div>
<div class="field">
<label for="f-beds">Min beds</label>
<select id="f-beds" name="beds">
<option value="Studio">Studio</option>
<option value="1+ bed">1+</option>
<option value="2+ beds">2+</option>
<option value="3+ beds" selected>3+</option>
<option value="4+ beds">4+</option>
</select>
</div>
<div class="field">
<label for="f-max">Max price</label>
<select id="f-max" name="price">
<option value="≤ $650k">$650k</option>
<option value="≤ $850k" selected>$850k</option>
<option value="≤ $1.2M">$1.2M</option>
<option value="≤ $1.8M">$1.8M</option>
<option value="≤ $3M">$3M</option>
</select>
</div>
<div class="field field--freq">
<span class="field__label">Notify</span>
<div class="seg" role="radiogroup" aria-label="Alert frequency">
<button type="button" class="seg__btn is-active" data-freq="Instant" role="radio" aria-checked="true">Instant</button>
<button type="button" class="seg__btn" data-freq="Daily" role="radio" aria-checked="false">Daily</button>
<button type="button" class="seg__btn" data-freq="Weekly" role="radio" aria-checked="false">Weekly</button>
</div>
</div>
<button type="submit" class="btn btn--primary create__submit">Save alert</button>
</form>
</section>
<!-- Saved searches list -->
<section class="panel" aria-labelledby="list-h">
<div class="list__head">
<h2 id="list-h" class="panel__title">Your saved searches <span id="count" class="panel__count">0</span></h2>
<div class="list__meta">
<span id="newtotal" class="newtotal" aria-live="polite">0 new matches</span>
</div>
</div>
<ul id="searches" class="searches" aria-label="Saved searches"></ul>
<p id="empty" class="empty" hidden>No saved searches yet — create an alert above to start tracking the market.</p>
</section>
</main>
<!-- Row template -->
<template id="row-tpl">
<li class="search" data-id="">
<div class="search__thumb" aria-hidden="true"><span class="search__tag"></span></div>
<div class="search__body">
<div class="search__top">
<h3 class="search__name"></h3>
<span class="badge badge--new" data-new hidden></span>
</div>
<div class="search__chips" aria-label="Search criteria"></div>
<p class="search__sub"></p>
</div>
<div class="search__controls">
<div class="seg seg--sm" role="radiogroup" aria-label="Notification frequency">
<button type="button" class="seg__btn" data-freq="Instant" role="radio">Instant</button>
<button type="button" class="seg__btn" data-freq="Daily" role="radio">Daily</button>
<button type="button" class="seg__btn" data-freq="Weekly" role="radio">Weekly</button>
</div>
<div class="search__actions">
<button type="button" class="icon-btn" data-act="clear" title="Clear new matches">Clear</button>
<button type="button" class="icon-btn" data-act="mute" title="Mute alerts" aria-pressed="false">Mute</button>
<button type="button" class="icon-btn icon-btn--danger" data-act="delete" title="Delete search" aria-label="Delete search">Delete</button>
</div>
</div>
</li>
</template>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Saved Searches / Alerts
A polished saved-searches and alerts surface for the fictional Marrow & Vale private-client brand. A “create a new alert” form sits at the top, letting a buyer name a search and pick an area, property type, minimum bedrooms, and a maximum price, then choose how often to be notified via an Instant / Daily / Weekly segmented control. Saving prepends a new row and fires a toast confirmation.
Each saved row reads like a listing card: a warm CSS-gradient “property photo” with a contextual tag, a serif headline, criteria chips for area, type, beds, and price, and a red new-match badge when fresh listings meet the brief. Per-row controls let you retune the alert frequency, clear the new-match badge, mute the search (dimming it and excluding it from the running total), or delete it with a soft exit animation.
The header keeps a live count of saved searches and a running tally of unmuted new matches, all driven by a small vanilla-JS state array with event delegation — no frameworks, no build step. The layout is fully responsive down to ~360px, where rows collapse to a single column and controls stretch to full width.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.