<div class="booking">
<h1>Book a viewing</h1>
<label for="date">Preferred date</label>
<div class="picker">
<input type="text" id="date" readonly placeholder="Choose a date" autocomplete="off">
<svg class="picker-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="3"></rect><path d="M16 2v4M8 2v4M3 10h18"></path></svg>
<div class="calendar" id="calendar" hidden>
<div class="calendar-head">
<button type="button" class="nav" id="prev" aria-label="Previous month">‹</button>
<span class="month" id="month" aria-live="polite"></span>
<button type="button" class="nav" id="next" aria-label="Next month">›</button>
</div>
<div class="weekdays" id="weekdays"></div>
<div class="days" id="days"></div>
</div>
</div>
<p class="note">Viewings run Monday to Saturday between 9am and 6pm.</p>
</div>
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background: #f1f5f9;
color: #0f172a;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 40px 20px;
}
.booking {
width: 100%;
max-width: 360px;
background: #ffffff;
padding: 32px;
border-radius: 20px;
box-shadow: 0 20px 40px -20px rgba(15, 23, 42, 0.25);
}
.booking h1 {
font-size: 1.375rem;
margin: 0 0 20px;
}
.booking label {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 6px;
}
.picker {
position: relative;
}
.picker input {
width: 100%;
font: inherit;
padding: 12px 42px 12px 14px;
border: 1.5px solid #cbd5e1;
border-radius: 10px;
background: #ffffff;
cursor: pointer;
outline: 0;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.picker input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15);
}
.picker-icon {
position: absolute;
right: 14px;
top: 14px;
color: #64748b;
pointer-events: none;
}
.calendar {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 296px;
background: #ffffff;
border-radius: 14px;
padding: 14px;
box-shadow: 0 20px 40px -12px rgba(15, 23, 42, 0.3);
z-index: 10;
}
.calendar-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.month {
font-weight: 600;
}
.nav {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
background: #f1f5f9;
color: #0f172a;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
}
.nav:hover {
background: #e2e8f0;
}
.weekdays, .days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.weekdays span {
text-align: center;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
padding: 4px 0;
}
.day {
aspect-ratio: 1;
border: 0;
border-radius: 8px;
background: transparent;
font: inherit;
font-size: 0.875rem;
color: #0f172a;
cursor: pointer;
}
.day:hover {
background: #eff6ff;
}
.day.today {
font-weight: 700;
color: #2563eb;
}
.day.selected {
background: #2563eb;
color: #ffffff;
}
.note {
margin: 16px 0 0;
font-size: 0.8125rem;
color: #64748b;
}
const input = document.getElementById("date");
const calendar = document.getElementById("calendar");
const monthLabel = document.getElementById("month");
const weekdays = document.getElementById("weekdays");
const days = document.getElementById("days");
const monthFormat = new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" });
const dayFormat = new Intl.DateTimeFormat(undefined, { weekday: "short" });
const fullFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "long" });
const today = new Date();
let viewYear = today.getFullYear();
let viewMonth = today.getMonth();
let selected = null;
for (let i = 0; i < 7; i++) {
const span = document.createElement("span");
span.textContent = dayFormat.format(new Date(2024, 0, 7 + i));
weekdays.appendChild(span);
}
function isSameDay(a, b) {
return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function render() {
monthLabel.textContent = monthFormat.format(new Date(viewYear, viewMonth, 1));
days.innerHTML = "";
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
const total = new Date(viewYear, viewMonth + 1, 0).getDate();
for (let i = 0; i < firstDay; i++) {
days.appendChild(document.createElement("span"));
}
for (let d = 1; d <= total; d++) {
const date = new Date(viewYear, viewMonth, d);
const button = document.createElement("button");
button.type = "button";
button.className = "day";
button.textContent = d;
button.setAttribute("aria-label", fullFormat.format(date));
if (isSameDay(date, today)) button.classList.add("today");
if (isSameDay(date, selected)) button.classList.add("selected");
button.addEventListener("click", function() {
selected = date;
input.value = fullFormat.format(date);
input.dataset.value = viewYear + "-" + String(viewMonth + 1).padStart(2, "0") + "-" + String(d).padStart(2, "0");
close();
input.focus();
});
days.appendChild(button);
}
}
function open() {
render();
calendar.hidden = false;
}
function close() {
calendar.hidden = true;
}
input.addEventListener("click", function() {
if (calendar.hidden) open(); else close();
});
input.addEventListener("keydown", function(e) {
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
e.preventDefault();
open();
}
});
document.getElementById("prev").addEventListener("click", function() {
viewMonth--;
if (viewMonth < 0) {
viewMonth = 11;
viewYear--;
}
render();
});
document.getElementById("next").addEventListener("click", function() {
viewMonth++;
if (viewMonth > 11) {
viewMonth = 0;
viewYear++;
}
render();
});
document.addEventListener("click", function(e) {
if (!e.target.closest(".picker")) close();
});
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") close();
});