ページ 普通
Hotel Booking — Room Detail
Room detail page with a switchable image gallery, amenities grid and selectable rate plans (flexible / non-refundable / bed & breakfast) feeding a sticky booking summary that recalculates the stay total live.
Labで開く
MCP
html css vanilla-js
ターゲット: JS HTML
コード
: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 {
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);
}
.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);
}
.crumbs {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.84rem;
color: rgba(251, 248, 242, 0.6);
}
.crumbs a {
color: rgba(251, 248, 242, 0.78);
text-decoration: none;
}
.crumbs a:hover {
color: var(--bone);
}
.crumbs strong {
color: var(--gold-light);
font-weight: 600;
}
/* ── Layout ── */
.detail {
max-width: 1180px;
margin: 0 auto;
padding: 28px 32px 72px;
display: grid;
grid-template-columns: 1fr 360px;
gap: 36px;
align-items: start;
}
/* ── Gallery ── */
.gallery {
margin-bottom: 30px;
}
.gallery-main {
height: 420px;
border-radius: var(--r-lg);
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #3a5180, #1a2b4a);
transition: background 0.3s;
}
.gallery-main::after {
content: attr(data-caption);
position: absolute;
left: 18px;
bottom: 16px;
background: rgba(15, 29, 54, 0.7);
color: var(--bone);
font-size: 0.78rem;
font-weight: 600;
padding: 7px 14px;
border-radius: 999px;
}
.gallery-main[data-shot="0"] {
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.gallery-main[data-shot="1"] {
background: linear-gradient(135deg, #6c7280, #2e3a52);
}
.gallery-main[data-shot="2"] {
background: linear-gradient(135deg, var(--gold-d), #5a4a1e);
}
.gallery-main[data-shot="3"] {
background: linear-gradient(135deg, #4a6da0, #243a5e);
}
.thumbs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 10px;
}
.thumb {
height: 76px;
border-radius: var(--r-md);
border: 2px solid transparent;
cursor: pointer;
position: relative;
overflow: hidden;
padding: 0;
}
.thumb[data-shot="0"] {
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.thumb[data-shot="1"] {
background: linear-gradient(135deg, #6c7280, #2e3a52);
}
.thumb[data-shot="2"] {
background: linear-gradient(135deg, var(--gold-d), #5a4a1e);
}
.thumb[data-shot="3"] {
background: linear-gradient(135deg, #4a6da0, #243a5e);
}
.thumb::after {
content: attr(data-label);
position: absolute;
inset: auto 0 0 0;
font-size: 0.62rem;
font-weight: 600;
color: var(--bone);
background: rgba(15, 29, 54, 0.55);
padding: 3px 0;
text-align: center;
}
.thumb.is-active {
border-color: var(--gold);
}
/* ── Head ── */
.eyebrow {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--gold-d);
font-weight: 700;
}
.head h1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 2.8rem;
color: var(--navy-d);
line-height: 1.05;
margin: 4px 0 8px;
}
.specs {
font-size: 0.92rem;
color: var(--warm-gray);
}
.rating {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 0.88rem;
}
.stars {
color: var(--gold);
letter-spacing: 1px;
}
.rating-num {
font-weight: 700;
color: var(--navy-d);
}
.rating-count {
color: var(--warm-gray);
}
/* ── Blocks ── */
.block {
margin-top: 30px;
padding-top: 26px;
border-top: 1px solid var(--line);
}
.block h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.7rem;
color: var(--navy-d);
margin-bottom: 14px;
}
.block p {
font-size: 0.96rem;
line-height: 1.7;
color: var(--ink-2);
max-width: 640px;
}
.amenities {
list-style: none;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 20px;
}
.amenities li {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.92rem;
color: var(--ink-2);
}
.am-ic {
width: 34px;
height: 34px;
flex-shrink: 0;
border-radius: var(--r-sm);
display: grid;
place-items: center;
background: rgba(201, 166, 73, 0.16);
color: var(--gold-d);
font-size: 1rem;
}
/* ── Rate plans ── */
.rates {
display: flex;
flex-direction: column;
gap: 12px;
}
.rate {
display: grid;
grid-template-columns: 24px 1fr auto;
align-items: center;
gap: 14px;
padding: 18px 20px;
background: var(--bone);
border: 1.5px solid var(--line);
border-radius: var(--r-md);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.rate input {
width: 20px;
height: 20px;
accent-color: var(--gold-d);
cursor: pointer;
}
.rate.is-selected {
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(201, 166, 73, 0.16);
}
.rate-name {
display: block;
font-size: 1.05rem;
font-weight: 600;
color: var(--navy-d);
}
.rate-policy {
display: block;
font-size: 0.82rem;
font-weight: 600;
margin-top: 3px;
}
.rate-policy.green {
color: var(--success);
}
.rate-policy.red {
color: var(--danger);
}
.rate-extras {
display: block;
font-size: 0.78rem;
color: var(--warm-gray);
margin-top: 2px;
}
.rate-price {
text-align: right;
white-space: nowrap;
}
.rate-price strong {
font-family: var(--font-display);
font-size: 1.7rem;
color: var(--navy-d);
}
.rate-price small {
display: block;
font-size: 0.74rem;
color: var(--warm-gray);
}
/* ── Booking sidebar ── */
.booking {
position: sticky;
top: 24px;
}
.booking-card {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: 22px 22px 24px;
}
.bc-eyebrow {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-d);
font-weight: 700;
margin-bottom: 12px;
}
.bc-dates {
display: flex;
align-items: center;
gap: 12px;
background: var(--cream);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
}
.bc-dates small {
display: block;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warm-gray);
font-weight: 600;
}
.bc-dates strong {
font-size: 0.92rem;
color: var(--navy-d);
font-weight: 600;
}
.bc-arrow {
color: var(--gold-d);
font-weight: 700;
}
.bc-meta {
font-size: 0.82rem;
color: var(--warm-gray);
margin: 12px 0 16px;
}
.bc-lines {
list-style: none;
border-top: 1px solid var(--line);
padding-top: 14px;
}
.bc-lines li {
display: flex;
justify-content: space-between;
font-size: 0.88rem;
color: var(--ink-2);
padding: 6px 0;
}
.num {
font-family: var(--font-mono);
font-weight: 700;
color: var(--navy-d);
font-variant-numeric: tabular-nums;
}
.num.green {
color: var(--success);
}
.bc-total {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: 12px;
padding-top: 14px;
border-top: 1px solid var(--line-strong);
}
.bc-total span {
font-size: 0.96rem;
font-weight: 600;
color: var(--ink);
}
.bc-total strong {
font-family: var(--font-display);
font-size: 2.1rem;
color: var(--navy-d);
font-variant-numeric: tabular-nums;
}
.bc-plan {
font-size: 0.78rem;
color: var(--success);
font-weight: 600;
margin: 8px 0 14px;
}
.reserve-btn {
width: 100%;
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-weight: 700;
font-size: 1rem;
padding: 14px;
border-radius: var(--r-md);
cursor: pointer;
}
.reserve-btn:hover {
background: var(--gold-light);
}
.bc-fine {
font-size: 0.74rem;
color: var(--warm-gray);
text-align: center;
margin-top: 10px;
}
/* ── 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: 940px) {
.detail {
grid-template-columns: 1fr;
}
.booking {
position: static;
}
.gallery-main {
height: 320px;
}
}
@media (max-width: 560px) {
.topbar {
padding: 12px 18px;
flex-wrap: wrap;
gap: 12px;
}
.detail {
padding: 22px 18px 56px;
}
.head h1 {
font-size: 2.2rem;
}
.amenities {
grid-template-columns: 1fr;
}
.rate {
grid-template-columns: 20px 1fr;
}
.rate-price {
grid-column: 2;
text-align: left;
}
}const NIGHTS = 3;
const TAX_RATE = 0.11;
const eur = (n) => "€" + n.toLocaleString("en-GB");
// ── Gallery ───────────────────────────────────────────────────────────────────
const main = document.getElementById("galleryMain");
const CAPTIONS = ["Bedroom", "Marble bathroom", "Skyline view", "Workspace"];
main.dataset.caption = CAPTIONS[0];
document.getElementById("thumbs").addEventListener("click", (e) => {
const t = e.target.closest(".thumb");
if (!t) return;
const shot = t.dataset.shot;
main.dataset.shot = shot;
main.dataset.caption = CAPTIONS[Number(shot)];
document.querySelectorAll(".thumb").forEach((x) => x.classList.remove("is-active"));
t.classList.add("is-active");
});
// ── Rate plan selection ───────────────────────────────────────────────────────
const rates = document.getElementById("rates");
const rateLine = document.getElementById("rateLine");
const subtotalEl = document.getElementById("subtotal");
const taxesEl = document.getElementById("taxes");
const totalEl = document.getElementById("total");
const bfLine = document.getElementById("bfLine");
const planNote = document.getElementById("planNote");
const fine = document.querySelector(".bc-fine");
const PLAN_META = {
flex: {
note: "Flexible rate · free cancellation",
fine: "You won't be charged yet on the flexible rate.",
},
bb: {
note: "Bed & breakfast · free cancellation",
fine: "Breakfast for 2 included · pay at the hotel.",
},
saver: {
note: "Non-refundable · charged at booking",
fine: "Non-refundable — your card is charged now.",
},
};
function recalc() {
const checked = rates.querySelector("input:checked");
const price = Number(checked.dataset.price);
const hasBf = checked.dataset.bf === "1";
const subtotal = price * NIGHTS;
const taxes = Math.round(subtotal * TAX_RATE);
const total = subtotal + taxes;
rateLine.textContent = `${eur(price)} × ${NIGHTS} nights`;
subtotalEl.textContent = eur(subtotal);
taxesEl.textContent = eur(taxes);
totalEl.textContent = eur(total);
bfLine.hidden = !hasBf;
const meta = PLAN_META[checked.value];
planNote.textContent = meta.note;
planNote.style.color = checked.value === "saver" ? "var(--danger)" : "var(--success)";
fine.textContent = meta.fine;
document
.querySelectorAll(".rate")
.forEach((r) => r.classList.toggle("is-selected", r.contains(checked)));
}
rates.addEventListener("change", recalc);
// ── Reserve ───────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2400);
}
document.getElementById("reserve").addEventListener("click", () => {
const plan = rates.querySelector("input:checked").value;
showToast(`Reserving Deluxe Double · ${PLAN_META[plan].note} · ${totalEl.textContent}`);
});
recalc();<!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>Deluxe Double · 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>
<nav class="crumbs">
<a href="#">Madrid</a><span>›</span><a href="#">Rooms</a><span>›</span><strong>Deluxe Double</strong>
</nav>
</header>
<main class="detail">
<div class="content">
<section class="gallery">
<div class="gallery-main" id="galleryMain" data-shot="0"></div>
<div class="thumbs" id="thumbs">
<button class="thumb is-active" data-shot="0" data-label="Bedroom" aria-label="Bedroom"></button>
<button class="thumb" data-shot="1" data-label="Bathroom" aria-label="Bathroom"></button>
<button class="thumb" data-shot="2" data-label="View" aria-label="View"></button>
<button class="thumb" data-shot="3" data-label="Workspace" aria-label="Workspace"></button>
</div>
</section>
<section class="head">
<p class="eyebrow">Deluxe collection</p>
<h1>Deluxe Double</h1>
<p class="specs">28m² · King bed · Sleeps 2 · City view · 3rd–6th floor</p>
<div class="rating">
<span class="stars">★★★★★</span>
<span class="rating-num">4.8</span>
<span class="rating-count">· 412 guest reviews</span>
</div>
</section>
<section class="block">
<h2>About this room</h2>
<p>
A calm, light-filled retreat above the city. The Deluxe Double pairs a plush king bed
with a marble bathroom, a generous work desk and floor-to-ceiling windows framing the
skyline. Soundproofed, climate-controlled, and finished in warm oak and brass.
</p>
</section>
<section class="block">
<h2>Room amenities</h2>
<ul class="amenities">
<li><span class="am-ic">◷</span>King bed · premium linen</li>
<li><span class="am-ic">≋</span>Rain shower & soaking tub</li>
<li><span class="am-ic">⚡</span>Fast Wi-Fi · 1 Gbps</li>
<li><span class="am-ic">☕</span>Nespresso & minibar</li>
<li><span class="am-ic">▦</span>55" smart TV</li>
<li><span class="am-ic">❄</span>Climate control</li>
<li><span class="am-ic">⌂</span>In-room safe</li>
<li><span class="am-ic">✦</span>Twice-daily housekeeping</li>
</ul>
</section>
<section class="block">
<h2>Choose your rate</h2>
<div class="rates" id="rates">
<label class="rate is-selected">
<input type="radio" name="rate" value="flex" data-price="184" data-bf="0" checked />
<span class="rate-body">
<span class="rate-name">Flexible rate</span>
<span class="rate-policy green">Free cancellation until 48h before arrival</span>
<span class="rate-extras">Pay at the hotel · no prepayment</span>
</span>
<span class="rate-price"><strong>€184</strong><small>/ night</small></span>
</label>
<label class="rate">
<input type="radio" name="rate" value="bb" data-price="206" data-bf="1" />
<span class="rate-body">
<span class="rate-name">Bed & breakfast</span>
<span class="rate-policy green">Free cancellation · breakfast for 2 included</span>
<span class="rate-extras">Buffet breakfast · 7:00–10:30</span>
</span>
<span class="rate-price"><strong>€206</strong><small>/ night</small></span>
</label>
<label class="rate">
<input type="radio" name="rate" value="saver" data-price="159" data-bf="0" />
<span class="rate-body">
<span class="rate-name">Non-refundable saver</span>
<span class="rate-policy red">No cancellation · pay now</span>
<span class="rate-extras">Best price · charged at booking</span>
</span>
<span class="rate-price"><strong>€159</strong><small>/ night</small></span>
</label>
</div>
</section>
</div>
<aside class="booking">
<div class="booking-card">
<p class="bc-eyebrow">Your stay</p>
<div class="bc-dates">
<div>
<small>Check-in</small>
<strong>Tue 9 Jun</strong>
</div>
<span class="bc-arrow">→</span>
<div>
<small>Check-out</small>
<strong>Fri 12 Jun</strong>
</div>
</div>
<p class="bc-meta">2 guests · 1 room · <span id="nightsMeta">3 nights</span></p>
<ul class="bc-lines">
<li><span id="rateLine">€184 × 3 nights</span><span class="num" id="subtotal">€552</span></li>
<li id="bfLine" hidden><span>Breakfast included</span><span class="num green">€0</span></li>
<li><span>Taxes & fees</span><span class="num" id="taxes">€61</span></li>
</ul>
<div class="bc-total">
<span>Total</span>
<strong id="total">€613</strong>
</div>
<p class="bc-plan" id="planNote">Flexible rate · free cancellation</p>
<button class="reserve-btn" id="reserve">Reserve now</button>
<p class="bc-fine">You won't be charged yet on the flexible rate.</p>
</div>
</aside>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Room Detail
The room page a guest lands on after picking a result. A gallery with a large hero and clickable thumbnails sits beside the room title, size and sleeping arrangement. Below, an amenities grid and a description. The heart of the page is the rate plan selector — flexible, non-refundable saver, and bed & breakfast — each a radio card with its own nightly price and policy. Selecting a plan updates the sticky booking summary (nightly rate × nights + taxes) and the reserve button in vanilla JS.