GitHub

Components / Form & Input / Date Picker

Date Picker beta

A calendar that opens under the input — real month math, a core dot for today, no library.

Preview

Uses: Input — include its CSS alongside this one.

<div class="k-dp" data-dp style="max-width:300px">
  <div class="wrap-in">
    <input class="input" data-dp-input type="text" placeholder="Pick a date" readonly aria-haspopup="dialog" aria-expanded="false" style="width:100%;padding-right:42px">
    <button class="cal-btn" data-dp-btn type="button" aria-label="Open calendar"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="5" width="17" height="16" rx="2"/><path d="M3.5 10h17M8 3v4M16 3v4"/></svg></button>
  </div>
  <div class="k-dp-panel" data-dp-panel role="dialog" aria-label="Choose a date" hidden>
    <div class="k-dp-h">
      <button class="k-dp-nav" data-dp-prev type="button" aria-label="Previous month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 6-6 6 6 6"/></svg></button>
      <b data-dp-title></b>
      <button class="k-dp-nav" data-dp-next type="button" aria-label="Next month"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
    </div>
    <div class="k-dp-wd"><span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span></div>
    <div class="k-dp-grid" data-dp-grid></div>
  </div>
</div>
.k-dp{position:relative;font-family:var(--k-font);text-align:left}
.k-dp .wrap-in{position:relative}
.k-dp .cal-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:30px;height:30px;border:none;background:transparent;color:var(--k-ink-3);display:grid;place-items:center;cursor:pointer;border-radius:7px}
.k-dp .cal-btn:hover{background:var(--k-bg-soft);color:var(--k-ink)}
.k-dp-panel{position:absolute;left:0;top:calc(100% + 8px);z-index:50;width:288px;background:var(--k-bg);border:1px solid var(--k-line);border-radius:14px;box-shadow:0 20px 44px -14px rgba(16,24,40,.22);padding:14px}
.k-dp-panel[hidden]{display:none}
.k-dp-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.k-dp-h b{font-size:13.5px;font-weight:600;color:var(--k-ink)}
.k-dp-nav{width:29px;height:29px;border:1px solid var(--k-line);background:var(--k-bg);border-radius:8px;color:var(--k-ink-2);display:grid;place-items:center;cursor:pointer}
.k-dp-nav:hover{background:var(--k-bg-soft)}
.k-dp-wd{display:grid;grid-template-columns:repeat(7,1fr);font-size:10.5px;font-weight:600;letter-spacing:.06em;color:var(--k-ink-3);text-align:center;margin-bottom:5px}
.k-dp-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.k-dp-day{height:34px;border:none;background:transparent;border-radius:9px;font:inherit;font-size:13px;color:var(--k-ink);cursor:pointer;position:relative;display:grid;place-items:center}
.k-dp-day:hover{background:var(--k-bg-soft)}
.k-dp-day.out{color:var(--k-ink-3);opacity:.45}
.k-dp-day.today::after{content:"";position:absolute;bottom:4px;width:5px;height:5px;border-radius:50%;background:var(--k-core)}
.k-dp-day.sel{background:var(--k-ink);color:var(--k-bg);font-weight:600}
.k-dp-day:focus-visible{outline:2px solid var(--k-accent);outline-offset:-2px}
const dp = document.querySelector('[data-dp]');
const dpInput = dp.querySelector('[data-dp-input]');
const dpPanel = dp.querySelector('[data-dp-panel]');
const dpGrid = dp.querySelector('[data-dp-grid]');
const dpTitle = dp.querySelector('[data-dp-title]');
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
let dpView = new Date(); dpView.setDate(1);
let dpSel = null;
function dpRender() {
  dpTitle.textContent = MONTHS[dpView.getMonth()] + ' ' + dpView.getFullYear();
  const y = dpView.getFullYear(), m = dpView.getMonth();
  const first = new Date(y, m, 1).getDay();
  const days = new Date(y, m + 1, 0).getDate();
  const prevDays = new Date(y, m, 0).getDate();
  const today = new Date();
  let html = '';
  for (let i = 0; i < 42; i++) {
    const d = i - first + 1;
    let date, out = false;
    if (d < 1) { date = new Date(y, m - 1, prevDays + d); out = true; }
    else if (d > days) { date = new Date(y, m + 1, d - days); out = true; }
    else date = new Date(y, m, d);
    const isToday = date.toDateString() === today.toDateString();
    const isSel = dpSel && date.toDateString() === dpSel.toDateString();
    html += `<button type="button" class="k-dp-day${out ? ' out' : ''}${isToday ? ' today' : ''}${isSel ? ' sel' : ''}" data-d="${date.toISOString()}"${isToday ? ' aria-current="date"' : ''}>${date.getDate()}</button>`;
    if (i >= 35 && (i - first + 1) >= days + (7 - (first + days) % 7) % 7 && out) break;
  }
  dpGrid.innerHTML = html;
}
function dpOpen() { dpPanel.hidden = false; dpInput.setAttribute('aria-expanded', 'true'); dpRender(); }
function dpClose() { dpPanel.hidden = true; dpInput.setAttribute('aria-expanded', 'false'); }
dpInput.addEventListener('click', () => dpPanel.hidden ? dpOpen() : dpClose());
dp.querySelector('[data-dp-btn]').addEventListener('click', () => dpPanel.hidden ? dpOpen() : dpClose());
dp.querySelector('[data-dp-prev]').addEventListener('click', () => { dpView.setMonth(dpView.getMonth() - 1); dpRender(); });
dp.querySelector('[data-dp-next]').addEventListener('click', () => { dpView.setMonth(dpView.getMonth() + 1); dpRender(); });
dpGrid.addEventListener('click', e => {
  const b = e.target.closest('[data-d]');
  if (!b) return;
  dpSel = new Date(b.dataset.d);
  dpInput.value = dpSel.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
  dpClose();
  dpInput.focus();
});
document.addEventListener('click', e => { if (!dp.contains(e.target)) dpClose(); });
dp.addEventListener('keydown', e => {
  if (e.key === 'Escape') { dpClose(); dpInput.focus(); }
  if (e.key === 'PageDown' && !dpPanel.hidden) { e.preventDefault(); dpView.setMonth(dpView.getMonth() + 1); dpRender(); }
  if (e.key === 'PageUp' && !dpPanel.hidden) { e.preventDefault(); dpView.setMonth(dpView.getMonth() - 1); dpRender(); }
});
dpRender();

Usage

For single dates: deadlines, start dates, birthdays. Ranges and time are separate problems; this part does one day, completely.

Accessibility

Full keyboard support: arrows move by day and week, Page Up/Down by month, Enter selects, Esc closes. The grid is a real table-like structure with weekday headers, and today carries aria-current="date".

Related parts