Seiten Medium
Hotel Booking — Room Results
Room results grid with a sticky stay summary bar, a filter sidebar (price range, room type, bed, amenities) and a sort control — all reacting live in vanilla JS to narrow and reorder the room cards.
In Lab öffnen
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--navy: #1a2b4a;
--navy-d: #0f1d36;
--navy-2: #2d4570;
--cream: #f7f3eb;
--cream-2: #ece5d4;
--bone: #fbf8f2;
--gold: #c9a649;
--gold-light: #e0c378;
--gold-d: #a88a2e;
--ink: #161e2c;
--ink-2: #2e3a52;
--warm-gray: #6c7280;
--line: rgba(22, 30, 44, 0.1);
--line-strong: rgba(22, 30, 44, 0.18);
--success: #4a7752;
--danger: #b34232;
--font-display: "Cormorant Garamond", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(22, 30, 44, 0.06), 0 2px 8px rgba(22, 30, 44, 0.06);
--shadow-2: 0 12px 36px rgba(15, 29, 54, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* ── Topbar ── */
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: var(--navy-d);
color: var(--bone);
padding: 14px 32px;
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--bone);
flex-shrink: 0;
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
background: var(--gold);
color: var(--navy-d);
font-family: var(--font-display);
font-weight: 700;
font-size: 1.2rem;
}
.brand-name {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 700;
}
.brand-name em {
font-style: normal;
color: var(--gold-light);
}
.stay-bar {
display: flex;
align-items: center;
gap: 26px;
margin-left: auto;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 999px;
padding: 8px 8px 8px 22px;
}
.stay-seg {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.seg-label {
font-size: 0.64rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--gold-light);
font-weight: 600;
}
.stay-seg strong {
font-size: 0.86rem;
font-weight: 600;
color: var(--bone);
}
.edit-btn {
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-size: 0.82rem;
font-weight: 700;
padding: 9px 16px;
border-radius: 999px;
cursor: pointer;
}
.edit-btn:hover {
background: var(--gold-light);
}
/* ── Layout ── */
.results {
max-width: 1200px;
margin: 0 auto;
padding: 28px 32px 64px;
display: grid;
grid-template-columns: 264px 1fr;
gap: 28px;
align-items: start;
}
/* ── Filters ── */
.filters {
position: sticky;
top: 92px;
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px 20px 20px;
}
.filters-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 14px;
border-bottom: 1px solid var(--line);
}
.filters-head h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.4rem;
color: var(--navy-d);
}
.clear-btn {
background: transparent;
border: none;
font-family: inherit;
font-size: 0.76rem;
font-weight: 600;
color: var(--navy-2);
cursor: pointer;
}
.clear-btn:hover {
color: var(--gold-d);
}
.fgroup {
padding: 16px 0;
border-bottom: 1px solid var(--line);
}
.fgroup:last-child {
border-bottom: none;
}
.fgroup h3 {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--gold-d);
font-weight: 700;
margin-bottom: 12px;
}
input[type="range"] {
width: 100%;
accent-color: var(--gold-d);
}
.range-out {
font-size: 0.82rem;
color: var(--warm-gray);
margin-top: 8px;
}
.range-out strong {
font-family: var(--font-mono);
color: var(--navy-d);
}
.check {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.88rem;
color: var(--ink-2);
padding: 7px 0;
cursor: pointer;
}
.check input {
width: 17px;
height: 17px;
accent-color: var(--navy);
cursor: pointer;
}
/* ── Listing ── */
.listing-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
gap: 16px;
}
.count {
font-size: 0.96rem;
color: var(--ink-2);
}
.count strong {
font-family: var(--font-display);
font-size: 1.4rem;
color: var(--navy-d);
}
.muted {
color: var(--warm-gray);
font-size: 0.86rem;
}
.sort {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
font-weight: 600;
color: var(--warm-gray);
}
.sort select {
font-family: inherit;
font-size: 0.86rem;
font-weight: 600;
color: var(--navy-d);
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
padding: 8px 12px;
cursor: pointer;
}
.room-list {
display: flex;
flex-direction: column;
gap: 18px;
}
.room {
display: grid;
grid-template-columns: 240px 1fr 200px;
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-1);
transition: box-shadow 0.18s, transform 0.18s;
}
.room:hover {
box-shadow: var(--shadow-2);
transform: translateY(-2px);
}
.room-img {
position: relative;
background: linear-gradient(135deg, var(--navy-2), var(--navy-d));
}
.room-img.t-standard {
background: linear-gradient(135deg, #6c7280, #2e3a52);
}
.room-img.t-deluxe {
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.room-img.t-suite {
background: linear-gradient(135deg, var(--gold-d), #5a4a1e);
}
.room-img::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 80% at 70% 20%, rgba(255, 255, 255, 0.12), transparent 60%);
}
.room-tag {
position: absolute;
top: 12px;
left: 12px;
background: rgba(15, 29, 54, 0.8);
color: var(--gold-light);
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 5px 10px;
border-radius: 999px;
}
.room-body {
padding: 18px 20px;
border-right: 1px solid var(--line);
}
.room-body h3 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.55rem;
color: var(--navy-d);
}
.room-specs {
font-size: 0.82rem;
color: var(--warm-gray);
margin: 4px 0 12px;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.badge {
font-size: 0.72rem;
font-weight: 600;
color: var(--ink-2);
background: var(--cream);
border: 1px solid var(--line);
padding: 4px 10px;
border-radius: 999px;
}
.badge.green {
color: var(--success);
background: rgba(74, 119, 82, 0.1);
border-color: rgba(74, 119, 82, 0.24);
}
.room-buy {
padding: 18px 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
text-align: right;
gap: 2px;
}
.price-night {
font-size: 0.78rem;
color: var(--warm-gray);
}
.price-total {
font-family: var(--font-display);
font-weight: 700;
font-size: 2rem;
color: var(--navy-d);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.price-sub {
font-size: 0.74rem;
color: var(--warm-gray);
margin-bottom: 12px;
}
.select-btn {
background: var(--navy);
color: var(--bone);
border: none;
font-family: inherit;
font-weight: 600;
font-size: 0.88rem;
padding: 11px 20px;
border-radius: var(--r-sm);
cursor: pointer;
width: 100%;
}
.select-btn:hover {
background: var(--navy-d);
}
.empty {
text-align: center;
padding: 70px 24px;
color: var(--warm-gray);
font-style: italic;
background: var(--bone);
border: 1px dashed var(--line-strong);
border-radius: var(--r-md);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 12px 22px;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
box-shadow: var(--shadow-2);
z-index: 50;
}
/* ── Responsive ── */
@media (max-width: 980px) {
.stay-bar {
display: none;
}
.edit-btn {
margin-left: auto;
}
.results {
grid-template-columns: 1fr;
}
.filters {
position: static;
}
.room {
grid-template-columns: 180px 1fr;
}
.room-buy {
grid-column: 1 / -1;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--line);
}
.room-buy .select-btn {
width: auto;
}
.price-sub {
margin-bottom: 0;
}
}
@media (max-width: 560px) {
.topbar {
padding: 12px 18px;
}
.results {
padding: 20px 18px 48px;
}
.room {
grid-template-columns: 1fr;
}
.room-img {
height: 150px;
}
.room-body {
border-right: none;
}
}// ── Room inventory (price is per night; stay = 3 nights) ──────────────────────
const NIGHTS = 3;
const ROOMS = [
{
id: "single-std",
name: "Classic Single",
type: "Standard",
bed: "Twin",
size: 18,
price: 132,
tag: "Smart value",
amenities: ["Refundable"],
specs: "City view · 18m² · 1 single bed",
},
{
id: "double-std",
name: "Classic Double",
type: "Standard",
bed: "Queen",
size: 24,
price: 168,
tag: null,
amenities: ["Breakfast", "Refundable"],
specs: "Courtyard view · 24m² · Queen bed",
},
{
id: "deluxe-double",
name: "Deluxe Double",
type: "Deluxe",
bed: "King",
size: 28,
price: 184,
tag: "Most booked",
amenities: ["Breakfast", "Balcony", "Refundable"],
specs: "City view · 28m² · King bed",
},
{
id: "twin-deluxe",
name: "Deluxe Twin",
type: "Deluxe",
bed: "Twin",
size: 30,
price: 196,
tag: null,
amenities: ["Breakfast", "Balcony"],
specs: "Garden view · 30m² · 2 twin beds",
},
{
id: "junior-suite",
name: "Junior Suite",
type: "Suite",
bed: "King",
size: 42,
price: 268,
tag: "Suite",
amenities: ["Breakfast", "Balcony", "Refundable"],
specs: "Balcony · 42m² · King + lounge area",
},
{
id: "signature-suite",
name: "Signature Suite",
type: "Suite",
bed: "King",
size: 64,
price: 412,
tag: "Top suite",
amenities: ["Breakfast", "Balcony", "Refundable"],
specs: "Terrace · 64m² · Separate living room",
},
];
// ── State ─────────────────────────────────────────────────────────────────────
const state = {
maxPrice: 520,
type: new Set(),
bed: new Set(),
amenity: new Set(),
sort: "rec",
};
const list = document.getElementById("roomList");
const empty = document.getElementById("empty");
const countEl = document.getElementById("count");
const toast = document.getElementById("toast");
const eur = (n) => "€" + n.toLocaleString("en-GB");
function passes(r) {
if (r.price > state.maxPrice) return false;
if (state.type.size && !state.type.has(r.type)) return false;
if (state.bed.size && !state.bed.has(r.bed)) return false;
for (const a of state.amenity) if (!r.amenities.includes(a)) return false;
return true;
}
function sortRooms(rooms) {
const s = [...rooms];
if (state.sort === "price-asc") s.sort((a, b) => a.price - b.price);
else if (state.sort === "price-desc") s.sort((a, b) => b.price - a.price);
else if (state.sort === "size-desc") s.sort((a, b) => b.size - a.size);
return s;
}
function render() {
const visible = sortRooms(ROOMS.filter(passes));
countEl.textContent = visible.length;
empty.hidden = visible.length > 0;
list.innerHTML = visible
.map((r) => {
const total = r.price * NIGHTS;
const badges = r.amenities
.map(
(a) =>
`<span class="badge ${a === "Refundable" ? "green" : ""}">${a === "Refundable" ? "Free cancellation" : a}</span>`
)
.join("");
return `
<article class="room">
<div class="room-img t-${r.type.toLowerCase()}">
${r.tag ? `<span class="room-tag">${r.tag}</span>` : ""}
</div>
<div class="room-body">
<h3>${r.name}</h3>
<p class="room-specs">${r.specs}</p>
<div class="badges">${badges}</div>
</div>
<div class="room-buy">
<p class="price-night">${eur(r.price)} / night</p>
<p class="price-total">${eur(total)}</p>
<p class="price-sub">total · ${NIGHTS} nights · incl. taxes</p>
<button class="select-btn" data-name="${r.name}">Select room</button>
</div>
</article>`;
})
.join("");
}
// ── Wiring ────────────────────────────────────────────────────────────────────
const priceRange = document.getElementById("priceRange");
const priceOut = document.getElementById("priceOut");
priceRange.addEventListener("input", () => {
state.maxPrice = Number(priceRange.value);
priceOut.textContent = eur(state.maxPrice);
render();
});
document.querySelectorAll('input[type="checkbox"][data-filter]').forEach((cb) => {
cb.addEventListener("change", () => {
const bucket = state[cb.dataset.filter];
if (cb.checked) bucket.add(cb.value);
else bucket.delete(cb.value);
render();
});
});
document.getElementById("sort").addEventListener("change", (e) => {
state.sort = e.target.value;
render();
});
document.getElementById("clearFilters").addEventListener("click", () => {
state.type.clear();
state.bed.clear();
state.amenity.clear();
state.maxPrice = 520;
state.sort = "rec";
priceRange.value = 520;
priceOut.textContent = eur(520);
document.getElementById("sort").value = "rec";
document
.querySelectorAll('input[type="checkbox"][data-filter]')
.forEach((cb) => (cb.checked = false));
render();
});
list.addEventListener("click", (e) => {
const btn = e.target.closest(".select-btn");
if (!btn) return;
showToast(`${btn.dataset.name} selected · continuing to room details…`);
});
document.querySelector(".edit-btn").addEventListener("click", () => showToast("Edit search"));
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2400);
}
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Available rooms · Aurelia Hotels</title>
</head>
<body>
<header class="topbar">
<a class="brand" href="#">
<span class="brand-mark">A</span>
<span class="brand-name">Aurelia <em>Hotels</em></span>
</a>
<div class="stay-bar">
<div class="stay-seg">
<span class="seg-label">Destination</span>
<strong>Aurelia · Madrid</strong>
</div>
<div class="stay-seg">
<span class="seg-label">Dates</span>
<strong>9 – 12 Jun · 3 nights</strong>
</div>
<div class="stay-seg">
<span class="seg-label">Guests</span>
<strong>2 guests · 1 room</strong>
</div>
<button class="edit-btn">Edit search</button>
</div>
</header>
<main class="results">
<aside class="filters">
<header class="filters-head">
<h2>Filters</h2>
<button class="clear-btn" id="clearFilters">Clear all</button>
</header>
<section class="fgroup">
<h3>Max price / night</h3>
<input type="range" id="priceRange" min="120" max="520" step="20" value="520" />
<p class="range-out">Up to <strong id="priceOut">€520</strong></p>
</section>
<section class="fgroup">
<h3>Room type</h3>
<label class="check"><input type="checkbox" data-filter="type" value="Standard" />Standard</label>
<label class="check"><input type="checkbox" data-filter="type" value="Deluxe" />Deluxe</label>
<label class="check"><input type="checkbox" data-filter="type" value="Suite" />Suite</label>
</section>
<section class="fgroup">
<h3>Bed</h3>
<label class="check"><input type="checkbox" data-filter="bed" value="King" />King</label>
<label class="check"><input type="checkbox" data-filter="bed" value="Queen" />Queen</label>
<label class="check"><input type="checkbox" data-filter="bed" value="Twin" />Twin</label>
</section>
<section class="fgroup">
<h3>Amenities</h3>
<label class="check"><input type="checkbox" data-filter="amenity" value="Breakfast" />Breakfast included</label>
<label class="check"><input type="checkbox" data-filter="amenity" value="Balcony" />Balcony</label>
<label class="check"><input type="checkbox" data-filter="amenity" value="Refundable" />Free cancellation</label>
</section>
</aside>
<section class="listing">
<header class="listing-head">
<p class="count"><strong id="count">0</strong> rooms available · <span class="muted">3-night stay</span></p>
<label class="sort">
Sort
<select id="sort">
<option value="rec">Recommended</option>
<option value="price-asc">Price · low to high</option>
<option value="price-desc">Price · high to low</option>
<option value="size-desc">Size · largest first</option>
</select>
</label>
</header>
<div class="room-list" id="roomList"></div>
<p class="empty" id="empty" hidden>No rooms match your filters. Try widening the price range.</p>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Room Results
The step after search: a results page listing available rooms for the chosen dates. A sticky summary bar echoes the stay (dates · nights · guests). The filter sidebar narrows by price range, room type, bed configuration and amenities; the sort control reorders by price or size. Every control updates the visible cards and the result count live in vanilla JS, with an empty state when filters exclude everything. Each card shows a price for the whole stay and a Select room call to action.