Real Estate — Schedule Tour / Open House
An editorial schedule-a-tour widget for a property listing, pairing a brass-accented serif header with a polished booking panel. Visitors switch between in-person and video showings, scan a seven-day date strip, and select from available time-slot chips while booked times are disabled. Contact fields gate a live summary and confirm button, and a separate open-house list lets guests RSVP. Built with semantic markup, ARIA roles, and vanilla JavaScript — no frameworks or network required.
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-1: 0 1px 2px rgba(22, 48, 42, 0.06), 0 1px 1px rgba(22, 48, 42, 0.04);
--sh-2: 0 6px 18px rgba(22, 48, 42, 0.08), 0 2px 6px rgba(22, 48, 42, 0.06);
--sh-3: 0 22px 48px rgba(22, 48, 42, 0.16), 0 8px 18px rgba(22, 48, 42, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--ivory);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 40px 20px;
}
h1, h2, h3 {
font-family: "Cormorant Garamond", Georgia, serif;
color: var(--green-d);
margin: 0;
letter-spacing: 0.01em;
}
.shell {
max-width: 980px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas:
"listing booker"
"open booker";
gap: 22px;
align-items: start;
}
/* ---------- Listing ---------- */
.listing {
grid-area: listing;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-2);
}
.photo {
position: relative;
aspect-ratio: 16 / 10;
background:
radial-gradient(120% 80% at 78% 18%, rgba(255, 233, 188, 0.55), transparent 55%),
radial-gradient(90% 70% at 12% 90%, rgba(31, 61, 52, 0.55), transparent 60%),
linear-gradient(160deg, #2b4f43 0%, #3c6450 32%, #b89466 70%, #d8b27e 100%);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px;
}
.photo::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, 0.12) 40%, transparent 43%),
repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.05) 0 2px, transparent 2px 56px);
mix-blend-mode: soft-light;
pointer-events: none;
}
.photo__label {
position: absolute;
top: 14px;
left: 16px;
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 0.78rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.photo__price {
position: relative;
z-index: 1;
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 700;
font-size: 1.7rem;
color: var(--white);
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
.badge {
display: inline-flex;
align-items: center;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 5px 10px;
border-radius: 999px;
}
.badge--status {
position: absolute;
top: 14px;
right: 14px;
z-index: 1;
background: var(--brass);
color: var(--white);
box-shadow: var(--sh-1);
}
.listing__body { padding: 20px 22px 24px; }
.eyebrow {
margin: 0 0 6px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--brass-d);
}
.listing__title { font-size: 2rem; line-height: 1.1; }
.listing__addr { margin: 4px 0 0; color: var(--muted); font-size: 0.92rem; }
.specs {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 18px 0;
padding: 0;
}
.specs li {
flex: 1 1 0;
min-width: 64px;
text-align: center;
background: var(--green-50);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 9px 4px;
}
.specs strong {
display: block;
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 1.35rem;
color: var(--green-d);
line-height: 1;
}
.specs span {
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.agent {
display: flex;
align-items: center;
gap: 12px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.agent__avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--green);
color: var(--brass-50);
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.04em;
}
.agent__meta { display: flex; flex-direction: column; line-height: 1.3; }
.agent__meta strong { font-size: 0.95rem; }
.agent__meta em { font-style: normal; font-size: 0.8rem; color: var(--muted); }
/* ---------- Booker ---------- */
.booker {
grid-area: booker;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
padding: 26px;
position: sticky;
top: 24px;
}
.booker__head h2 { font-size: 1.85rem; line-height: 1.05; }
.booker__head p { margin: 4px 0 0; color: var(--muted); font-size: 0.9rem; }
.field { margin-top: 20px; }
.field__label {
display: block;
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-2);
margin-bottom: 9px;
}
.field__hint {
font-weight: 500;
letter-spacing: 0;
text-transform: none;
color: var(--muted);
font-size: 0.78rem;
}
/* Toggle */
.toggle {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
background: var(--ivory);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 5px;
}
.toggle__opt {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: 0;
border-radius: 10px;
padding: 10px 8px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.toggle__opt .ico { width: 17px; height: 17px; fill: currentColor; }
.toggle__opt:hover { color: var(--green-d); }
.toggle__opt.is-active {
background: var(--green);
color: var(--white);
box-shadow: var(--sh-1);
}
/* Date strip */
.dates {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(0, 1fr);
gap: 7px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: thin;
}
.date {
flex: 0 0 auto;
text-align: center;
border: 1px solid var(--line);
background: var(--paper);
border-radius: var(--r-md);
padding: 9px 4px;
cursor: pointer;
transition: transform 0.14s ease, border-color 0.18s ease, background 0.18s ease;
min-width: 52px;
}
.date:hover { transform: translateY(-2px); border-color: var(--line-2); }
.date .dow {
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.date .dnum {
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 1.35rem;
font-weight: 600;
color: var(--green-d);
line-height: 1.1;
}
.date .mon {
font-size: 0.62rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--brass-d);
}
.date.is-active {
background: var(--green);
border-color: var(--green);
}
.date.is-active .dow,
.date.is-active .mon { color: var(--brass-50); }
.date.is-active .dnum { color: var(--white); }
/* Slots */
.slots {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.slot {
font-family: inherit;
font-size: 0.88rem;
font-weight: 600;
color: var(--green-700);
background: var(--green-50);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 6px;
cursor: pointer;
transition: transform 0.14s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.slot:hover:not(:disabled) { transform: translateY(-2px); border-color: var(--brass); }
.slot.is-active {
background: var(--brass);
border-color: var(--brass);
color: var(--white);
box-shadow: var(--sh-1);
}
.slot:disabled {
cursor: not-allowed;
color: var(--muted);
background: var(--ivory);
border-style: dashed;
text-decoration: line-through;
opacity: 0.7;
}
.slots__empty {
grid-column: 1 / -1;
color: var(--muted);
font-size: 0.88rem;
padding: 6px 0;
}
/* Inputs */
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ifield { display: flex; flex-direction: column; gap: 6px; }
.ifield > span {
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-2);
}
.ifield input {
font-family: inherit;
font-size: 0.95rem;
color: var(--ink);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 11px 13px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.ifield input::placeholder { color: var(--muted); }
.ifield input:focus {
outline: none;
border-color: var(--green);
box-shadow: 0 0 0 3px var(--green-50);
}
/* Summary + CTA */
.summary {
display: flex;
align-items: center;
gap: 10px;
margin-top: 22px;
padding: 12px 14px;
background: var(--ivory);
border: 1px solid var(--line);
border-left: 3px solid var(--brass);
border-radius: var(--r-sm);
font-size: 0.9rem;
color: var(--ink-2);
}
.summary__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--muted);
flex: 0 0 auto;
transition: background 0.2s ease;
}
.summary.is-ready { border-left-color: var(--ok); color: var(--ink); }
.summary.is-ready .summary__dot { background: var(--ok); box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.18); }
.cta {
width: 100%;
margin-top: 14px;
font-family: inherit;
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--white);
background: linear-gradient(180deg, var(--green-700), var(--green-d));
border: 1px solid var(--green-d);
border-radius: var(--r-md);
padding: 14px;
cursor: pointer;
transition: transform 0.14s ease, box-shadow 0.18s ease, filter 0.18s ease;
box-shadow: var(--sh-2);
}
.cta:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--sh-3); }
.cta:active:not(:disabled) { transform: translateY(0); }
.cta:disabled {
cursor: not-allowed;
filter: grayscale(0.5);
opacity: 0.55;
box-shadow: none;
}
/* ---------- Open houses ---------- */
.open {
grid-area: open;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 22px;
}
.open__head h2 { font-size: 1.6rem; }
.open__head p { margin: 3px 0 0; color: var(--muted); font-size: 0.88rem; }
.open__list { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 11px; }
.oh {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 13px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.14s ease;
}
.oh:hover { border-color: var(--line-2); box-shadow: var(--sh-1); transform: translateY(-1px); }
.oh__cal {
width: 50px;
text-align: center;
border: 1px solid var(--line);
border-radius: var(--r-sm);
overflow: hidden;
flex: 0 0 auto;
}
.oh__cal .m {
display: block;
background: var(--green);
color: var(--brass-50);
font-size: 0.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 2px 0;
}
.oh__cal .d {
display: block;
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 1.4rem;
font-weight: 600;
color: var(--green-d);
padding: 2px 0;
}
.oh__info { min-width: 0; }
.oh__info b { font-size: 0.95rem; color: var(--ink); display: block; }
.oh__info span { font-size: 0.82rem; color: var(--muted); }
.oh__count {
display: inline-block;
margin-top: 3px;
font-size: 0.72rem;
font-weight: 600;
color: var(--brass-d);
}
.oh__btn {
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--green-d);
background: var(--brass-50);
border: 1px solid var(--brass);
border-radius: 999px;
padding: 8px 15px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, transform 0.14s ease;
flex: 0 0 auto;
}
.oh__btn:hover:not(:disabled) { transform: translateY(-1px); }
.oh__btn.is-going {
background: var(--green);
border-color: var(--green);
color: var(--white);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 140%);
background: var(--green-d);
color: var(--paper);
padding: 13px 20px;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--sh-3);
border: 1px solid var(--green-700);
max-width: calc(100vw - 40px);
opacity: 0;
transition: transform 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.32s ease;
z-index: 50;
}
.toast.show { transform: translate(-50%, 0); opacity: 1; }
.toast b { color: var(--brass-50); }
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.shell {
grid-template-columns: 1fr;
grid-template-areas:
"listing"
"booker"
"open";
}
.booker { position: static; }
}
@media (max-width: 520px) {
body { padding: 22px 14px; }
.listing__title { font-size: 1.6rem; }
.booker { padding: 20px; }
.booker__head h2 { font-size: 1.55rem; }
.photo__price { font-size: 1.4rem; }
.grid2 { grid-template-columns: 1fr; }
.slots { grid-template-columns: repeat(2, 1fr); }
.specs li { min-width: 0; flex-basis: 22%; }
.oh { grid-template-columns: auto 1fr; }
.oh__btn { grid-column: 2; justify-self: start; margin-top: 4px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.innerHTML = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 3600);
}
/* ---------- State ---------- */
var state = { type: "in-person", date: null, slot: null };
var DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var BASE_SLOTS = ["9:00 AM", "10:30 AM", "12:00 PM", "1:30 PM", "3:00 PM", "4:30 PM", "6:00 PM"];
// Deterministic "booked" slots per day so the UI feels real and disables some.
function bookedFor(dayIndex) {
var picks = {
0: ["12:00 PM", "4:30 PM"],
1: ["10:30 AM"],
2: ["9:00 AM", "1:30 PM", "6:00 PM"],
3: ["3:00 PM"],
4: ["10:30 AM", "4:30 PM"],
5: ["9:00 AM", "12:00 PM", "3:00 PM"],
6: ["1:30 PM"]
};
return picks[dayIndex] || [];
}
/* ---------- Elements ---------- */
var dateStrip = document.getElementById("dateStrip");
var slotGrid = document.getElementById("slotGrid");
var slotHint = document.getElementById("slotHint");
var summary = document.getElementById("summary");
var summaryText = document.getElementById("summaryText");
var confirmBtn = document.getElementById("confirmBtn");
var typeBtns = document.querySelectorAll(".toggle__opt");
/* ---------- Build date strip (next 7 days) ---------- */
var days = [];
function buildDates() {
var today = new Date();
for (var i = 0; i < 7; i++) {
var d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + i);
days.push(d);
var el = document.createElement("button");
el.type = "button";
el.className = "date";
el.setAttribute("role", "option");
el.setAttribute("aria-selected", "false");
el.dataset.idx = String(i);
el.innerHTML =
'<span class="dow">' + (i === 0 ? "Today" : DOW[d.getDay()]) + "</span>" +
'<span class="dnum">' + d.getDate() + "</span>" +
'<span class="mon">' + MON[d.getMonth()] + "</span>";
el.addEventListener("click", function () {
selectDate(parseInt(this.dataset.idx, 10));
});
dateStrip.appendChild(el);
}
}
function selectDate(idx) {
state.date = idx;
state.slot = null;
Array.prototype.forEach.call(dateStrip.children, function (c, i) {
var on = i === idx;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", on ? "true" : "false");
});
renderSlots(idx);
update();
}
/* ---------- Slots ---------- */
function renderSlots(idx) {
slotGrid.innerHTML = "";
var booked = bookedFor(idx);
var available = 0;
BASE_SLOTS.forEach(function (time) {
var b = document.createElement("button");
b.type = "button";
b.className = "slot";
b.setAttribute("role", "option");
b.textContent = time;
if (booked.indexOf(time) !== -1) {
b.disabled = true;
b.title = "Already booked";
b.setAttribute("aria-disabled", "true");
} else {
available++;
b.setAttribute("aria-selected", "false");
b.addEventListener("click", function () {
state.slot = time;
Array.prototype.forEach.call(slotGrid.querySelectorAll(".slot"), function (s) {
var on = s === b;
s.classList.toggle("is-active", on);
if (!s.disabled) s.setAttribute("aria-selected", on ? "true" : "false");
});
update();
});
}
slotGrid.appendChild(b);
});
slotHint.textContent = "(" + available + " of " + BASE_SLOTS.length + " open)";
}
/* ---------- Tour type toggle ---------- */
Array.prototype.forEach.call(typeBtns, function (btn) {
btn.addEventListener("click", function () {
state.type = this.dataset.type;
Array.prototype.forEach.call(typeBtns, function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
update();
});
});
/* ---------- Summary / readiness ---------- */
function prettyDate(idx) {
var d = days[idx];
return DOW[d.getDay()] + ", " + MON[d.getMonth()] + " " + d.getDate();
}
function update() {
var ready = state.date !== null && state.slot !== null;
var typeLabel = state.type === "video" ? "Video tour" : "In-person tour";
if (ready) {
summaryText.innerHTML =
"<strong>" + typeLabel + "</strong> · " +
prettyDate(state.date) + " at <strong>" + state.slot + "</strong>";
summary.classList.add("is-ready");
} else {
var parts = [];
if (state.date === null) parts.push("a date");
if (state.slot === null) parts.push("a time");
summaryText.textContent =
typeLabel + " selected — now pick " + parts.join(" and ") + ".";
summary.classList.remove("is-ready");
}
confirmBtn.disabled = !ready;
}
/* ---------- Confirm ---------- */
confirmBtn.addEventListener("click", function () {
if (confirmBtn.disabled) return;
var name = (document.getElementById("fName").value || "").trim();
var email = (document.getElementById("fEmail").value || "").trim();
if (!name) { toast("Please add your name so the agent can reach you."); return; }
if (!email || email.indexOf("@") === -1) { toast("Please enter a valid email address."); return; }
var typeLabel = state.type === "video" ? "Video tour" : "In-person tour";
toast("<b>Tour requested</b> · " + typeLabel + " on " + prettyDate(state.date) + " at " + state.slot);
// Lock the chosen slot as if newly booked.
var chosen = slotGrid.querySelector(".slot.is-active");
if (chosen) {
chosen.classList.remove("is-active");
chosen.disabled = true;
chosen.title = "Booked by you";
}
state.slot = null;
update();
renderHint();
});
function renderHint() {
if (state.date === null) return;
var open = slotGrid.querySelectorAll(".slot:not(:disabled)").length;
slotHint.textContent = "(" + open + " of " + BASE_SLOTS.length + " open)";
}
/* ---------- Open houses ---------- */
var openHouses = [
{ mon: "Jun", day: 14, title: "Sunset Open House", when: "Sat 1:00 – 4:00 PM", going: 9 },
{ mon: "Jun", day: 18, title: "Twilight Walkthrough", when: "Wed 5:30 – 7:00 PM", going: 4 },
{ mon: "Jun", day: 21, title: "Weekend Showcase", when: "Sat 11:00 AM – 2:00 PM", going: 12 }
];
var openList = document.getElementById("openList");
openHouses.forEach(function (oh, i) {
var li = document.createElement("li");
li.className = "oh";
li.innerHTML =
'<span class="oh__cal"><span class="m">' + oh.mon + '</span><span class="d">' + oh.day + "</span></span>" +
'<span class="oh__info"><b>' + oh.title + "</b><span>" + oh.when + "</span>" +
'<span class="oh__count" data-count>' + oh.going + " attending</span></span>" +
'<button type="button" class="oh__btn">RSVP</button>';
var btn = li.querySelector(".oh__btn");
var countEl = li.querySelector("[data-count]");
var going = false;
btn.addEventListener("click", function () {
going = !going;
oh.going += going ? 1 : -1;
countEl.textContent = oh.going + " attending";
btn.classList.toggle("is-going", going);
btn.textContent = going ? "Going ✓" : "RSVP";
toast(going
? "<b>You're on the list</b> for " + oh.title + " · " + oh.when
: "RSVP cancelled for " + oh.title);
});
openList.appendChild(li);
});
/* ---------- Init ---------- */
buildDates();
selectDate(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Schedule a Tour — Maplewood Residence</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">
<!-- ============ LISTING HEADER ============ -->
<section class="listing" aria-label="Property summary">
<div class="photo" role="img" aria-label="Architectural photo of a modern terraced residence at dusk">
<span class="photo__label">Maplewood Residence</span>
<span class="badge badge--status">For Sale</span>
<div class="photo__price">$1,485,000</div>
</div>
<div class="listing__body">
<p class="eyebrow">Schedule a private showing</p>
<h1 class="listing__title">14 Maplewood Crescent</h1>
<p class="listing__addr">Highgrove Park · Westbridge, OR 97214</p>
<ul class="specs" aria-label="Property specifications">
<li><strong>4</strong><span>Beds</span></li>
<li><strong>3.5</strong><span>Baths</span></li>
<li><strong>3,240</strong><span>Sq Ft</span></li>
<li><strong>0.31</strong><span>Acre</span></li>
</ul>
<div class="agent">
<span class="agent__avatar" aria-hidden="true">EM</span>
<span class="agent__meta">
<strong>Elena Marsh</strong>
<em>Listing Agent · Crescent & Vale Realty</em>
</span>
</div>
</div>
</section>
<!-- ============ BOOKING WIDGET ============ -->
<section class="booker" aria-label="Schedule a tour">
<header class="booker__head">
<h2>Book your tour</h2>
<p>Choose how you'd like to view the home, then pick a day and time.</p>
</header>
<!-- Tour type -->
<div class="field">
<span class="field__label" id="lbl-type">Tour type</span>
<div class="toggle" role="radiogroup" aria-labelledby="lbl-type">
<button type="button" class="toggle__opt is-active" role="radio" aria-checked="true" data-type="in-person">
<svg viewBox="0 0 24 24" aria-hidden="true" class="ico"><path d="M12 2 3 9v12h6v-6h6v6h6V9z"/></svg>
In-person
</button>
<button type="button" class="toggle__opt" role="radio" aria-checked="false" data-type="video">
<svg viewBox="0 0 24 24" aria-hidden="true" class="ico"><path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11z"/></svg>
Video
</button>
</div>
</div>
<!-- Date strip -->
<div class="field">
<span class="field__label">Select a day</span>
<div class="dates" id="dateStrip" role="listbox" aria-label="Choose a tour date"></div>
</div>
<!-- Time slots -->
<div class="field">
<span class="field__label">Available times <span class="field__hint" id="slotHint"></span></span>
<div class="slots" id="slotGrid" role="listbox" aria-label="Choose a time slot"></div>
</div>
<!-- Contact -->
<div class="field grid2">
<label class="ifield">
<span>Full name</span>
<input type="text" id="fName" placeholder="Jordan Avery" autocomplete="name" />
</label>
<label class="ifield">
<span>Phone</span>
<input type="tel" id="fPhone" placeholder="(555) 012-8841" autocomplete="tel" />
</label>
</div>
<div class="field">
<label class="ifield">
<span>Email</span>
<input type="email" id="fEmail" placeholder="[email protected]" autocomplete="email" />
</label>
</div>
<!-- Summary + submit -->
<div class="summary" id="summary" aria-live="polite">
<span class="summary__dot" aria-hidden="true"></span>
<span id="summaryText">Select a tour type, date and time to continue.</span>
</div>
<button type="button" class="cta" id="confirmBtn" disabled>Confirm tour request</button>
</section>
<!-- ============ OPEN HOUSES ============ -->
<section class="open" aria-label="Upcoming open houses">
<header class="open__head">
<h2>Open houses</h2>
<p>Drop in — no appointment needed. RSVP so we can greet you.</p>
</header>
<ul class="open__list" id="openList"></ul>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Schedule Tour / Open House
A premium, editorial tour-booking widget for a single property listing. The left column presents the home — a gradient “listing photograph”, a brass status badge, price, specs (beds, baths, sq ft, lot), and the listing agent — while a sticky booking panel handles the appointment. A radio-style toggle switches between In-person and Video tours, a horizontal strip shows the next seven days, and a grid of time-slot chips lets buyers pick a time. Slots that are already taken render disabled and struck-through, so availability reads at a glance.
The flow is driven by a small amount of vanilla JavaScript. Selecting a tour type, date, and time keeps a live summary in sync; the Confirm tour request button only enables once a valid slot is chosen and name and email are filled in. Confirming fires a toast, then locks the chosen slot as if it were freshly booked. A dedicated open-house list sits below the listing card, where each event can be RSVP’d — toggling the button updates the attendee count and surfaces a confirmation toast.
Everything is self-contained: two Google Fonts, hand-tuned CSS gradients standing in for property
photography, accessible role/aria-* attributes on the controls, AA-contrast colors, and a responsive
layout that collapses to a single column and reflows the slot grid down to ~360px.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.