UI Components Hard
Date Picker
Calendar date picker with month navigation, range selection, disabled dates, and an input trigger — keyboard accessible.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--card2: #111827;
--border: rgba(255, 255, 255, 0.08);
--text: #f2f6ff;
--muted: #475569;
--accent: #38bdf8;
--accent-bg: rgba(56, 189, 248, 0.14);
--range-bg: rgba(56, 189, 248, 0.07);
--today-dot: #38bdf8;
--radius: 12px;
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
padding: 3rem 1.5rem;
}
.page {
max-width: 560px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.demo-block {
display: flex;
flex-direction: column;
gap: 0.625rem;
position: relative;
}
/* Ensure earlier blocks stack above later ones so popups aren't clipped */
.demo-block:nth-child(1) {
z-index: 3;
}
.demo-block:nth-child(2) {
z-index: 2;
}
.demo-block:nth-child(3) {
z-index: 1;
}
.demo-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
}
/* ── Input wrap ── */
.dp-wrap,
.dp-range-wrap {
position: relative;
}
.dp-range-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dp-input-wrap {
position: relative;
flex: 1;
}
.dp-input {
width: 100%;
background: var(--card);
border: 1.5px solid var(--border);
color: var(--text);
padding: 0.6rem 2.5rem 0.6rem 0.875rem;
border-radius: 10px;
font-size: 0.875rem;
cursor: pointer;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.dp-input:focus,
.dp-input[aria-expanded="true"] {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.12);
}
.dp-icon {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.9rem;
pointer-events: none;
opacity: 0.55;
}
.dp-range-arrow {
color: var(--muted);
font-size: 0.85rem;
flex-shrink: 0;
}
/* ── Calendar popup ── */
.dp-calendar {
position: absolute;
top: calc(100% + 8px);
left: 0;
background: var(--card2);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
padding: 1rem;
min-width: 280px;
z-index: 500;
animation: cal-in 0.14s ease;
}
@keyframes cal-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dp-calendar[hidden] {
display: none;
}
.dp-calendar--range {
left: 0;
}
/* Inline calendar */
.dp-calendar--inline {
position: static;
box-shadow: none;
border: 1px solid var(--border);
display: block;
}
.dp-inline {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.dp-inline-value {
font-size: 0.85rem;
color: var(--muted);
text-align: center;
}
/* ── Calendar header ── */
.cal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.875rem;
}
.cal-nav-btn {
width: 28px;
height: 28px;
border-radius: 6px;
background: none;
border: 1px solid var(--border);
color: var(--text);
font-size: 1rem;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.2s, border-color 0.2s;
}
.cal-nav-btn:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.2);
}
.cal-month-label {
font-size: 0.9rem;
font-weight: 600;
}
/* ── Weekday row ── */
.cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 4px;
}
.cal-weekday {
font-size: 0.68rem;
font-weight: 600;
text-align: center;
color: var(--muted);
padding: 0.2rem 0;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Days grid ── */
.cal-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.cal-day {
position: relative;
width: 100%;
aspect-ratio: 1;
border-radius: 6px;
background: none;
border: none;
color: var(--text);
font-size: 0.82rem;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s, color 0.15s;
outline: none;
}
.cal-day:hover:not([disabled]):not(.cal-day--selected):not(.cal-day--selected-end) {
background: rgba(255, 255, 255, 0.06);
}
.cal-day:focus-visible {
box-shadow: 0 0 0 2px var(--accent);
}
/* Ghost days (prev/next month) */
.cal-day--ghost {
color: rgba(71, 85, 105, 0.4);
cursor: default;
pointer-events: none;
}
/* Disabled */
.cal-day[disabled] {
color: rgba(71, 85, 105, 0.35);
cursor: not-allowed;
pointer-events: none;
}
/* Today dot */
.cal-day--today::after {
content: "";
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background: var(--today-dot);
border-radius: 50%;
}
/* Selected (single or range endpoints) */
.cal-day--selected,
.cal-day--selected-end {
background: var(--accent);
color: #050910;
font-weight: 700;
}
.cal-day--selected:hover,
.cal-day--selected-end:hover {
background: var(--accent);
}
/* In-range */
.cal-day--in-range {
background: var(--range-bg);
border-radius: 0;
}
/* Range start/end connectors */
.cal-day--selected.cal-day--range-start {
border-radius: 6px 0 0 6px;
}
.cal-day--selected-end {
border-radius: 0 6px 6px 0;
}(function () {
"use strict";
const DAYS_OF_WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
// ── Helpers ─────────────────────────────────────────────────────────────────
function today() {
const d = new Date();
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function sameDay(a, b) {
return a && b && a.toDateString() === b.toDateString();
}
function isBefore(a, b) {
return a && b && a < b;
}
function isBetween(d, start, end) {
if (!start || !end) return false;
const lo = isBefore(start, end) ? start : end;
const hi = isBefore(start, end) ? end : start;
return d > lo && d < hi;
}
function formatDate(d) {
if (!d) return "";
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function daysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
// ── Calendar builder ─────────────────────────────────────────────────────────
function buildCalendar(calEl, opts) {
// opts: { year, month, selected, rangeStart, rangeEnd, hoverDate, onSelect, onHover, disablePast }
calEl.innerHTML = "";
const year = opts.year;
const month = opts.month;
const firstDay = new Date(year, month, 1).getDay(); // 0 = Sunday
const totalDays = daysInMonth(year, month);
const now = today();
// Header
const header = el("div", "cal-header");
const prevBtn = el("button", "cal-nav-btn");
prevBtn.innerHTML = "‹";
prevBtn.setAttribute("aria-label", "Previous month");
prevBtn.addEventListener("click", function () {
opts.onMonthChange(-1);
});
const nextBtn = el("button", "cal-nav-btn");
nextBtn.innerHTML = "›";
nextBtn.setAttribute("aria-label", "Next month");
nextBtn.addEventListener("click", function () {
opts.onMonthChange(1);
});
const label = el("span", "cal-month-label");
label.textContent = MONTHS[month] + " " + year;
header.appendChild(prevBtn);
header.appendChild(label);
header.appendChild(nextBtn);
calEl.appendChild(header);
// Weekdays
const wdRow = el("div", "cal-weekdays");
DAYS_OF_WEEK.forEach(function (d) {
const wd = el("span", "cal-weekday");
wd.textContent = d;
wdRow.appendChild(wd);
});
calEl.appendChild(wdRow);
// Days
const grid = el("div", "cal-days");
// Ghost days from prev month
const prevMonthDays = daysInMonth(year, month - 1);
for (let i = firstDay - 1; i >= 0; i--) {
const ghost = el("button", "cal-day cal-day--ghost");
ghost.textContent = prevMonthDays - i;
ghost.disabled = true;
grid.appendChild(ghost);
}
// Real days
for (let d = 1; d <= totalDays; d++) {
const date = new Date(year, month, d);
const btn = el("button", "cal-day");
btn.textContent = d;
btn.setAttribute("data-date", date.toISOString());
btn.setAttribute("aria-label", formatDate(date));
// Today
if (sameDay(date, now)) btn.classList.add("cal-day--today");
// Disabled (past dates)
if (opts.disablePast && date < now) {
btn.disabled = true;
btn.classList.add("cal-day--disabled");
}
// Selected single
if (!opts.rangeStart && sameDay(date, opts.selected)) {
btn.classList.add("cal-day--selected");
}
// Range: start
if (opts.rangeStart && sameDay(date, opts.rangeStart)) {
btn.classList.add("cal-day--selected", "cal-day--range-start");
}
// Range: end
const effectiveEnd = opts.rangeEnd || opts.hoverDate;
if (
opts.rangeStart &&
effectiveEnd &&
sameDay(date, effectiveEnd) &&
!sameDay(date, opts.rangeStart)
) {
btn.classList.add("cal-day--selected-end");
}
// Range: in-between
if (opts.rangeStart && isBetween(date, opts.rangeStart, effectiveEnd)) {
btn.classList.add("cal-day--in-range");
}
btn.addEventListener("click", function () {
if (opts.onSelect) opts.onSelect(date);
});
if (opts.onHover) {
btn.addEventListener("mouseenter", function () {
opts.onHover(date);
});
}
grid.appendChild(btn);
}
// Ghost days for next month to fill the grid
const totalCells = Math.ceil((firstDay + totalDays) / 7) * 7;
for (let i = 1; i <= totalCells - firstDay - totalDays; i++) {
const ghost = el("button", "cal-day cal-day--ghost");
ghost.textContent = i;
ghost.disabled = true;
grid.appendChild(ghost);
}
calEl.appendChild(grid);
}
function el(tag, cls) {
const e = document.createElement(tag);
if (cls) e.className = cls;
return e;
}
// ── Demo 1: Single date picker ───────────────────────────────────────────────
(function () {
const input = document.getElementById("single-input");
const calEl = document.getElementById("cal-single");
const wrap = document.getElementById("dp-single");
let open = false;
let selected = null;
let viewYear = today().getFullYear();
let viewMonth = today().getMonth();
function render() {
buildCalendar(calEl, {
year: viewYear,
month: viewMonth,
selected: selected,
disablePast: false,
onMonthChange: function (d) {
viewMonth += d;
if (viewMonth > 11) {
viewMonth = 0;
viewYear++;
}
if (viewMonth < 0) {
viewMonth = 11;
viewYear--;
}
render();
},
onSelect: function (date) {
selected = date;
input.value = formatDate(date);
close();
},
});
}
function open_() {
open = true;
calEl.removeAttribute("hidden");
input.setAttribute("aria-expanded", "true");
render();
}
function close() {
open = false;
calEl.setAttribute("hidden", "");
input.setAttribute("aria-expanded", "false");
}
input.addEventListener("click", function () {
open ? close() : open_();
});
input.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
open ? close() : open_();
}
if (e.key === "Escape") close();
});
document.addEventListener("click", function (e) {
if (!wrap.contains(e.target)) close();
});
})();
// ── Demo 2: Date range picker ────────────────────────────────────────────────
(function () {
const startInput = document.getElementById("range-start");
const endInput = document.getElementById("range-end");
const calEl = document.getElementById("cal-range");
const wrap = document.getElementById("dp-range");
let open = false;
let rangeStart = null;
let rangeEnd = null;
let hoverDate = null;
let picking = "start"; // "start" | "end"
let viewYear = today().getFullYear();
let viewMonth = today().getMonth();
function render() {
buildCalendar(calEl, {
year: viewYear,
month: viewMonth,
rangeStart: rangeStart,
rangeEnd: rangeEnd,
hoverDate: hoverDate,
disablePast: false,
onMonthChange: function (d) {
viewMonth += d;
if (viewMonth > 11) {
viewMonth = 0;
viewYear++;
}
if (viewMonth < 0) {
viewMonth = 11;
viewYear--;
}
render();
},
onSelect: function (date) {
if (picking === "start") {
rangeStart = date;
rangeEnd = null;
picking = "end";
startInput.value = formatDate(date);
endInput.value = "";
} else {
// If end before start, swap
if (date < rangeStart) {
rangeEnd = rangeStart;
rangeStart = date;
} else {
rangeEnd = date;
}
picking = "start";
startInput.value = formatDate(rangeStart);
endInput.value = formatDate(rangeEnd);
close();
}
render();
},
onHover: function (date) {
if (picking === "end") {
hoverDate = date;
render();
}
},
});
}
function open_() {
open = true;
calEl.removeAttribute("hidden");
render();
}
function close() {
open = false;
calEl.setAttribute("hidden", "");
}
[startInput, endInput].forEach(function (inp) {
inp.addEventListener("click", function () {
open ? close() : open_();
});
});
document.addEventListener("click", function (e) {
if (!wrap.contains(e.target)) close();
});
})();
// ── Demo 3: Inline calendar ──────────────────────────────────────────────────
(function () {
const calEl = document.getElementById("cal-inline");
const valueEl = document.getElementById("inline-value");
let selected = null;
let viewYear = today().getFullYear();
let viewMonth = today().getMonth();
function render() {
buildCalendar(calEl, {
year: viewYear,
month: viewMonth,
selected: selected,
disablePast: false,
onMonthChange: function (d) {
viewMonth += d;
if (viewMonth > 11) {
viewMonth = 0;
viewYear++;
}
if (viewMonth < 0) {
viewMonth = 11;
viewYear--;
}
render();
},
onSelect: function (date) {
selected = date;
valueEl.textContent = "Selected: " + formatDate(date);
render();
},
});
}
render();
})();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Date Picker</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- ── Demo 1: Single date picker ── -->
<div class="demo-block">
<label class="demo-label" for="single-input">Single date picker</label>
<div class="dp-wrap" id="dp-single">
<div class="dp-input-wrap">
<input
type="text"
id="single-input"
class="dp-input"
placeholder="Select a date…"
readonly
aria-haspopup="true"
aria-expanded="false"
aria-label="Date picker"
/>
<span class="dp-icon" aria-hidden="true">📅</span>
</div>
<div class="dp-calendar" id="cal-single" role="dialog" aria-label="Date picker calendar" hidden></div>
</div>
</div>
<!-- ── Demo 2: Date range picker ── -->
<div class="demo-block">
<label class="demo-label">Date range picker</label>
<div class="dp-range-wrap" id="dp-range">
<div class="dp-input-wrap">
<input
type="text"
id="range-start"
class="dp-input dp-input--range"
placeholder="Start date…"
readonly
aria-label="Range start date"
/>
<span class="dp-icon" aria-hidden="true">📅</span>
</div>
<span class="dp-range-arrow" aria-hidden="true">→</span>
<div class="dp-input-wrap">
<input
type="text"
id="range-end"
class="dp-input dp-input--range"
placeholder="End date…"
readonly
aria-label="Range end date"
/>
<span class="dp-icon" aria-hidden="true">📅</span>
</div>
<div class="dp-calendar dp-calendar--range" id="cal-range" role="dialog" aria-label="Date range calendar" hidden></div>
</div>
</div>
<!-- ── Demo 3: Inline calendar ── -->
<div class="demo-block">
<p class="demo-label">Inline calendar</p>
<div class="dp-inline" id="dp-inline">
<div class="dp-calendar dp-calendar--inline" id="cal-inline"></div>
<p class="dp-inline-value" id="inline-value">No date selected</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Date Picker
A full calendar date picker with month navigation, single-date and range selection, disabled past dates, and an inline variant. No external libraries.
Variants
- Single date — click an input to open a calendar dropdown, select a date, input updates
- Date range — two inputs (start / end); first click sets start, second click sets end, hover highlights the range
- Inline calendar — always visible, no trigger input
How it works
- The calendar grid is generated dynamically: first-day offset + days-in-month cells, padded with previous/next month ghost days
- Month navigation arrows call
changeMonth(delta)which re-renders the grid - For range mode, two state variables track
rangeStartandrangeEnd;mouseoveron a day cell updates ahoverDateused for the in-range highlight - Past dates receive the
disabledattribute and class; they are skipped during keyboard navigation - Arrow keys move focus cell-by-cell;
Enterselects;Escapecloses the dropdown
Day cell states
- Today — dot indicator below the number
- Selected / range endpoints — solid accent background
- In-range — light accent tint background
- Disabled — muted text, not interactive