UI Components Hard
Date Range Picker
A dual-calendar date range picker with hover preview, preset ranges, and keyboard navigation. Used for booking, analytics date filters, and reporting.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
/* ── Demo label ── */
.drp-demo {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
.drp-demo-label {
font-size: 0.8125rem;
font-weight: 500;
color: #94a3b8;
margin-bottom: 0.25rem;
}
/* ── Trigger ── */
.drp-trigger-wrap {
position: relative;
}
.drp-trigger {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.6875rem 1rem;
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.75rem;
color: #e2e8f0;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
min-width: 280px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.drp-trigger:hover {
border-color: rgba(56, 189, 248, 0.3);
}
.drp-trigger[aria-expanded="true"] {
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.1);
}
.drp-trigger-icon {
color: #64748b;
flex-shrink: 0;
}
.drp-chevron {
color: #64748b;
margin-left: auto;
flex-shrink: 0;
transition: transform 0.2s;
}
.drp-trigger[aria-expanded="true"] .drp-chevron {
transform: rotate(180deg);
}
/* ── Dropdown ── */
.drp-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
background: #141e2e;
border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: 1rem;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
z-index: 100;
opacity: 0;
transform: translateY(-6px) scale(0.98);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
min-width: 560px;
}
.drp-dropdown.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
.drp-dropdown-inner {
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: inherit;
}
/* ── Calendars row ── */
.drp-calendars {
display: flex;
padding: 1.25rem 1.5rem;
gap: 1rem;
}
.drp-cal-divider {
width: 1px;
background: rgba(255, 255, 255, 0.06);
margin: 0;
align-self: stretch;
}
/* ── Single calendar ── */
.drp-cal {
flex: 1;
min-width: 220px;
}
.drp-cal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.drp-cal-title {
font-size: 0.9375rem;
font-weight: 700;
color: #f1f5f9;
}
.drp-nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.375rem;
color: #94a3b8;
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.drp-nav-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.drp-cal-dow {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-bottom: 0.375rem;
}
.drp-cal-dow span {
text-align: center;
font-size: 0.6875rem;
font-weight: 600;
color: #64748b;
padding: 0.25rem 0;
text-transform: uppercase;
}
/* ── Calendar grid ── */
.drp-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
}
.drp-day {
display: flex;
align-items: center;
justify-content: center;
height: 34px;
font-size: 0.8125rem;
color: #cbd5e1;
cursor: pointer;
transition: background 0.1s, color 0.1s;
border-radius: 0.3rem;
position: relative;
user-select: none;
font-weight: 500;
}
.drp-day:hover:not(.other-month):not(.disabled):not(.range-start):not(.range-end) {
background: rgba(56, 189, 248, 0.14);
color: #f1f5f9;
}
.drp-day.other-month {
color: #243044;
cursor: default;
pointer-events: none;
}
.drp-day.disabled {
color: #243044;
cursor: not-allowed;
}
.drp-day.today::after {
content: "";
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
width: 3px;
height: 3px;
border-radius: 50%;
background: #38bdf8;
}
.drp-day.in-range {
background: rgba(56, 189, 248, 0.18);
border-radius: 0;
color: #bae6fd;
}
.drp-day.range-start,
.drp-day.range-end {
background: #38bdf8 !important;
color: #0f172a !important;
font-weight: 700;
z-index: 1;
}
.drp-day.range-start {
border-radius: 0.375rem 0 0 0.375rem;
}
.drp-day.range-end {
border-radius: 0 0.375rem 0.375rem 0;
}
.drp-day.range-start.range-end {
border-radius: 0.375rem;
}
.drp-day.hover-range {
background: rgba(56, 189, 248, 0.12);
border-radius: 0;
color: #e2e8f0;
}
/* ── Preset pills row ── */
.drp-presets {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.preset-btn {
padding: 0.3125rem 0.75rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
color: #94a3b8;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.12s;
white-space: nowrap;
font-family: inherit;
}
.preset-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #f1f5f9;
}
.preset-btn.active {
background: rgba(56, 189, 248, 0.15);
border-color: rgba(56, 189, 248, 0.3);
color: #38bdf8;
}
/* ── Footer ── */
.drp-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
gap: 1rem;
}
.drp-footer-range {
font-size: 0.8125rem;
color: #94a3b8;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.drp-footer-actions {
display: flex;
gap: 0.5rem;
}
.btn-primary {
padding: 0.5rem 1rem;
background: #38bdf8;
border: none;
border-radius: 0.5rem;
color: #0f172a;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
font-family: inherit;
}
.btn-primary:hover {
background: #7dd3fc;
}
.btn-secondary {
padding: 0.5rem 0.875rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}(function () {
"use strict";
/* ── State ── */
const TODAY = new Date(2026, 2, 2); // March 2, 2026 for demo
let leftYear = TODAY.getFullYear();
let leftMonth = TODAY.getMonth() - 1; // Feb
if (leftMonth < 0) {
leftMonth = 11;
leftYear--;
}
let rightYear = TODAY.getFullYear();
let rightMonth = TODAY.getMonth(); // Mar
let startDate = null;
let endDate = null;
let hoverDate = null;
let selecting = false; // true = waiting for end date
// Pre-select a default range for demo
startDate = new Date(2026, 1, 15); // Feb 15
endDate = new Date(2026, 1, 22); // Feb 22
/* ── Elements ── */
const trigger = document.getElementById("drp-trigger");
const triggerText = document.getElementById("drp-trigger-text");
const dropdown = document.getElementById("drp-dropdown");
const leftGrid = document.getElementById("drp-left-grid");
const rightGrid = document.getElementById("drp-right-grid");
const leftTitle = document.getElementById("drp-left-title");
const rightTitle = document.getElementById("drp-right-title");
const footerRange = document.getElementById("drp-footer-range");
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const SHORT_MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
/* ── Open/close ── */
trigger.addEventListener("click", toggleDropdown);
document.addEventListener("click", (e) => {
if (!dropdown.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) {
closeDropdown();
}
});
document.getElementById("drp-cancel").addEventListener("click", closeDropdown);
document.getElementById("drp-apply").addEventListener("click", applyRange);
function toggleDropdown() {
const isOpen = dropdown.classList.contains("open");
isOpen ? closeDropdown() : openDropdown();
}
function openDropdown() {
dropdown.classList.add("open");
trigger.setAttribute("aria-expanded", "true");
dropdown.removeAttribute("aria-hidden");
renderBoth();
}
function closeDropdown() {
dropdown.classList.remove("open");
trigger.setAttribute("aria-expanded", "false");
dropdown.setAttribute("aria-hidden", "true");
hoverDate = null;
selecting = false;
}
function applyRange() {
updateTriggerText();
closeDropdown();
}
/* ── Navigation ── */
document.getElementById("drp-prev").addEventListener("click", () => {
leftMonth--;
if (leftMonth < 0) {
leftMonth = 11;
leftYear--;
}
rightMonth = leftMonth + 1;
rightYear = leftYear;
if (rightMonth > 11) {
rightMonth = 0;
rightYear++;
}
renderBoth();
});
document.getElementById("drp-next").addEventListener("click", () => {
rightMonth++;
if (rightMonth > 11) {
rightMonth = 0;
rightYear++;
}
leftMonth = rightMonth - 1;
leftYear = rightYear;
if (leftMonth < 0) {
leftMonth = 11;
leftYear--;
}
renderBoth();
});
/* ── Grid-level hover delegation (survives DOM rebuilds) ── */
function attachGridHover(grid) {
grid.addEventListener("mouseover", (e) => {
const btn = e.target.closest(".drp-day");
if (!btn || btn.classList.contains("other-month") || btn.disabled) return;
const ts = parseInt(btn.dataset.ts);
if (!hoverDate || hoverDate.getTime() !== ts) {
hoverDate = new Date(ts);
renderBoth();
}
});
grid.addEventListener("mouseleave", () => {
if (hoverDate !== null) {
hoverDate = null;
renderBoth();
}
});
}
attachGridHover(leftGrid);
attachGridHover(rightGrid);
/* ── Render ── */
function renderBoth() {
leftTitle.textContent = `${MONTHS[leftMonth]} ${leftYear}`;
rightTitle.textContent = `${MONTHS[rightMonth]} ${rightYear}`;
renderMonth(leftGrid, leftYear, leftMonth);
renderMonth(rightGrid, rightYear, rightMonth);
updateFooter();
}
function renderMonth(grid, year, month) {
grid.innerHTML = "";
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDays = new Date(year, month, 0).getDate();
for (let i = 0; i < 42; i++) {
const btn = document.createElement("button");
btn.className = "drp-day";
let d;
if (i < firstDay) {
d = new Date(year, month - 1, prevMonthDays - firstDay + i + 1);
btn.classList.add("other-month");
btn.disabled = true;
} else if (i >= firstDay + daysInMonth) {
d = new Date(year, month + 1, i - firstDay - daysInMonth + 1);
btn.classList.add("other-month");
btn.disabled = true;
} else {
d = new Date(year, month, i - firstDay + 1);
if (isSameDay(d, TODAY)) btn.classList.add("today");
}
btn.textContent = d.getDate();
btn.dataset.ts = d.getTime();
if (!btn.classList.contains("other-month")) {
applyRangeClasses(btn, d);
}
btn.addEventListener("click", () => onDayClick(d));
grid.appendChild(btn);
}
}
function applyRangeClasses(btn, d) {
const rangeStart = startDate;
// Guard: only use hoverDate for preview when a start date is already selected
const rangeEnd =
selecting && hoverDate && startDate ? (hoverDate >= startDate ? hoverDate : null) : endDate;
if (rangeStart && isSameDay(d, rangeStart)) btn.classList.add("range-start");
if (rangeEnd && isSameDay(d, rangeEnd)) btn.classList.add("range-end");
if (rangeStart && rangeEnd && d > rangeStart && d < rangeEnd) {
btn.classList.add("in-range");
}
// Hover preview strip (only when actively selecting and start is set)
if (selecting && hoverDate && startDate && hoverDate >= startDate) {
if (d > startDate && d < hoverDate) {
btn.classList.add("hover-range");
}
}
}
function onDayClick(d) {
if (!selecting || !startDate) {
// First click — set start
startDate = d;
endDate = null;
selecting = true;
} else {
// Second click — set end
if (d < startDate) {
endDate = startDate;
startDate = d;
} else {
endDate = d;
}
selecting = false;
}
// Mark custom preset as active
document.querySelectorAll(".preset-btn").forEach((b) => b.classList.remove("active"));
const customBtn = document.querySelector('[data-preset="custom"]');
if (customBtn) customBtn.classList.add("active");
renderBoth();
}
/* ── Footer & trigger text ── */
function updateFooter() {
if (startDate && endDate) {
const days = Math.round((endDate - startDate) / 86400000) + 1;
footerRange.textContent = `${fmtShort(startDate)} → ${fmtShort(endDate)} · ${days} day${days !== 1 ? "s" : ""}`;
} else if (startDate && selecting) {
footerRange.textContent = `${fmtShort(startDate)} → …`;
} else {
footerRange.textContent = "No date selected";
}
}
function updateTriggerText() {
if (startDate && endDate) {
triggerText.textContent = `${fmtShort(startDate)} – ${fmtShort(endDate)}`;
} else if (startDate) {
triggerText.textContent = fmtShort(startDate);
} else {
triggerText.textContent = "Select date range";
}
}
/* ── Presets ── */
document.getElementById("drp-presets").addEventListener("click", (e) => {
const btn = e.target.closest(".preset-btn");
if (!btn) return;
document.querySelectorAll(".preset-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const preset = btn.dataset.preset;
const now = new Date(TODAY);
if (preset === "custom") {
// Enter manual selection mode — clear so user can pick fresh
startDate = null;
endDate = null;
selecting = false;
hoverDate = null;
updateTriggerText();
renderBoth();
return;
}
switch (preset) {
case "today":
startDate = new Date(now);
endDate = new Date(now);
break;
case "yesterday": {
const y = new Date(now);
y.setDate(y.getDate() - 1);
startDate = y;
endDate = new Date(y);
break;
}
case "last7": {
const s = new Date(now);
s.setDate(s.getDate() - 6);
startDate = s;
endDate = new Date(now);
break;
}
case "last30": {
const s = new Date(now);
s.setDate(s.getDate() - 29);
startDate = s;
endDate = new Date(now);
break;
}
case "thisMonth":
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
break;
case "lastMonth":
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth(), 0);
break;
case "thisQuarter": {
const qStart = Math.floor(now.getMonth() / 3) * 3;
startDate = new Date(now.getFullYear(), qStart, 1);
endDate = new Date(now.getFullYear(), qStart + 3, 0);
break;
}
}
selecting = false;
hoverDate = null;
// Navigate left calendar to show start date
if (startDate) {
leftYear = startDate.getFullYear();
leftMonth = startDate.getMonth();
rightMonth = leftMonth + 1;
rightYear = leftYear;
if (rightMonth > 11) {
rightMonth = 0;
rightYear++;
}
}
renderBoth();
});
/* ── Keyboard ── */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeDropdown();
});
/* ── Helpers ── */
function isSameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function fmtShort(d) {
return `${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
/* ── Init ── */
updateTriggerText();
renderBoth();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Date Range Picker</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="drp-demo">
<p class="drp-demo-label">Date Range</p>
<div class="drp-trigger-wrap">
<button class="drp-trigger" id="drp-trigger" aria-expanded="false" aria-haspopup="true">
<svg class="drp-trigger-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>
</svg>
<span id="drp-trigger-text">Select date range</span>
<svg class="drp-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m6 9 6 6 6-6"/></svg>
</button>
<!-- Dropdown -->
<div class="drp-dropdown" id="drp-dropdown" aria-hidden="true">
<div class="drp-dropdown-inner">
<!-- Calendars -->
<div class="drp-calendars">
<!-- Left calendar -->
<div class="drp-cal" id="drp-cal-left">
<div class="drp-cal-header">
<button class="drp-nav-btn" id="drp-prev" aria-label="Previous month">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span class="drp-cal-title" id="drp-left-title"></span>
</div>
<div class="drp-cal-dow">
<span>Su</span><span>Mo</span><span>Tu</span>
<span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
</div>
<div class="drp-cal-grid" id="drp-left-grid"></div>
</div>
<div class="drp-cal-divider"></div>
<!-- Right calendar -->
<div class="drp-cal" id="drp-cal-right">
<div class="drp-cal-header">
<span class="drp-cal-title" id="drp-right-title"></span>
<button class="drp-nav-btn" id="drp-next" aria-label="Next month">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
<div class="drp-cal-dow">
<span>Su</span><span>Mo</span><span>Tu</span>
<span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
</div>
<div class="drp-cal-grid" id="drp-right-grid"></div>
</div>
</div>
<!-- Preset pills row -->
<div class="drp-presets" id="drp-presets">
<button class="preset-btn" data-preset="today">Today</button>
<button class="preset-btn" data-preset="yesterday">Yesterday</button>
<button class="preset-btn" data-preset="last7">Last 7 Days</button>
<button class="preset-btn" data-preset="last30">Last 30 Days</button>
<button class="preset-btn" data-preset="thisMonth">This Month</button>
<button class="preset-btn" data-preset="lastMonth">Last Month</button>
<button class="preset-btn" data-preset="thisQuarter">This Quarter</button>
<button class="preset-btn active" data-preset="custom">Custom</button>
</div>
<!-- Footer -->
<div class="drp-footer">
<span class="drp-footer-range" id="drp-footer-range">No date selected</span>
<div class="drp-footer-actions">
<button class="btn-secondary" id="drp-cancel">Cancel</button>
<button class="btn-primary" id="drp-apply">Apply</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Date Range Picker
A polished dual-calendar date range picker for SaaS dashboards and booking interfaces. Click a start date then hover to preview the range in real time before confirming the end date. Preset range shortcuts cover common analytics windows, and keyboard navigation with arrow keys + Enter makes it fully accessible without a mouse.
Features
- Two side-by-side month calendars with independent navigation
- Real-time hover preview between start and end selection
- 8 preset range shortcuts (Today, Last 7 Days, This Month, etc.)
- Disabled past dates toggle and out-of-month day dimming
- Today marker, selected range highlight, and accent end-caps
- Footer showing selected range, day count, Cancel and Apply buttons
- Keyboard navigation: arrow keys to move focus, Enter to select, Escape to close