About this snippet

Click the field and a calendar drops down for the current month. Step through months with the arrows, pick a day and the field fills with the date, or press Escape to close without choosing.

The calendar is rebuilt every time the month changes. The script works out which weekday the month starts on and how many days it has, pads the front of the grid with blank cells and renders one button per day, so every date is a real focusable control. Month and weekday names come from Intl.DateTimeFormat, so the picker reads correctly in whatever language the browser is set to. The chosen date goes into the input as a readable string and into a data attribute in ISO format for a form to submit.

Clicking outside the picker closes it through one listener on the document, and the field is readonly so a phone keyboard does not pop up over the calendar.

Size
6.1 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
28
<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">&lsaquo;</button>
        <span class="month" id="month" aria-live="polite"></span>
        <button type="button" class="nav" id="next" aria-label="Next month">&rsaquo;</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();
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save