날짜 선택기

Date picker

입력창을 누르면 달력이 팝업으로 열리고, 날짜 하나를 고르면 값이 채워지며 닫히는 폼 컨트롤.

다른 이름: DatepickerDate inputCalendar picker
···
html
<div class="dpw">
  <span class="dpf">약속일</span>
  <button class="dpin" id="dpin" aria-haspopup="dialog" aria-expanded="true">2026년 9월 26일 ▾</button>
  <div class="dpop" role="dialog" aria-label="날짜 선택">
    <div class="dphead"><button aria-label="이전 달" tabindex="-1">‹</button><b>2026년 9월</b><button aria-label="다음 달" tabindex="-1">›</button></div>
    <div class="dpgrid" role="grid" aria-label="9월" id="dpgrid">
      <span class="dow">일</span><span class="dow">월</span><span class="dow">화</span><span class="dow">수</span><span class="dow">목</span><span class="dow">금</span><span class="dow">토</span>
    </div>
  </div>
</div>
css
.dpw{width:min(230px,92%);display:flex;flex-direction:column;gap:6px;font-size:11px}
.dpf{color:var(--muted);font-size:10px;margin-left:4px}
.dpin{width:100%;text-align:left;padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--fg);font-size:12px;font-weight:600;cursor:default}
.dpop{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px;display:flex;flex-direction:column;gap:6px}
.dphead{display:flex;align-items:center;justify-content:space-between;font-size:11px;font-weight:700;color:var(--fg)}
.dphead button{border:0;background:none;color:var(--muted);font-size:14px;cursor:default;width:20px}
.dpgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.dow{text-align:center;color:var(--muted);font-size:9px;padding:2px 0}
.dcell{border:0;background:none;color:var(--fg);font-size:10px;border-radius:6px;cursor:default;aspect-ratio:1;display:grid;place-items:center}
.dcell.today{color:var(--accent);font-weight:700}
.dcell[aria-selected=true]{background:var(--accent);color:#fff;font-weight:700}
js
const grid = document.getElementById('dpgrid');
const label = document.getElementById('dpin');
const FIRST_DOW = 2, DAYS = 30;
const cells = [];
for (let i = 0; i < FIRST_DOW; i++) grid.appendChild(document.createElement('span'));
for (let d = 1; d <= DAYS; d++) {
  const b = document.createElement('button');
  b.className = 'dcell'; b.type = 'button'; b.setAttribute('role', 'gridcell'); b.tabIndex = -1;
  b.textContent = String(d);
  if (d === 26) b.classList.add('today');
  grid.appendChild(b);
  cells.push(b);
}
function select(d) {
  cells.forEach((c, i) => { if (i + 1 === d) c.setAttribute('aria-selected', 'true'); else c.removeAttribute('aria-selected'); });
  label.textContent = '2026년 9월 ' + d + '일 ▾';
}
let auto = true, sel = 26, dir = 1;
select(sel);
cells.forEach((c, i) => c.addEventListener('click', () => { auto = false; select(i + 1); }));
setInterval(() => {
  if (!auto) return;
  sel += dir;
  if (sel >= 30) dir = -1;
  if (sel <= 1) dir = 1;
  select(sel);
}, 900);

트리거에는 aria-haspopup="dialog"와 aria-expanded를, 팝업 안 달력 그리드에는 role="grid"를 주고 날짜 버튼 하나하나가 role="gridcell"이 됩니다. 방향키 상하좌우로 하루씩(위/아래는 7일씩) 이동하고, PageUp/PageDown으로 한 달 전/후로 건너뛰는 게 관례입니다.

캘린더 뷰와 헷갈리기 쉽지만 쓰임이 다릅니다. 캘린더 뷰는 한 달치 일정을 계속 펼쳐놓고 훑어보는 화면 자체인 반면, 데이트 피커는 폼 입력창 하나에 값을 채우기 위해 잠깐 열렸다 닫히는 보조 팝업입니다. 값을 고르면 바로 닫히는 게 원칙이라 여러 날짜를 동시에 훑어볼 필요는 없습니다.

포커스는 팝업이 열릴 때 이미 선택된 날짜(없으면 오늘)로 이동하고, 닫히면 다시 입력창으로 돌아와야 합니다. Esc로 닫히며, 이때는 값이 바뀌지 않아야 합니다.