日付ピッカー

入力欄をクリックするとカレンダーがふわりとポップし、日付を選ぶと入力欄に反映されて閉じます。外側クリックでも自然に閉じる予約フォーム向けの部品です。

#css#js#calendar

ライブデモ

コード

HTML
<!-- 日付ピッカー: 入力欄をクリックするとカレンダーがポップし、日付を選ぶと入力欄に反映されて閉じます -->
<div class="cdp-stage">
  <div class="cdp-field" id="cdpField">
    <label class="cdp-label" for="cdpInput">お届け希望日</label>
    <div class="cdp-control">
      <input
        type="text"
        id="cdpInput"
        class="cdp-input"
        placeholder="日付を選択してください"
        readonly
        role="combobox"
        aria-haspopup="dialog"
        aria-expanded="false"
        aria-controls="cdpPanel"
        autocomplete="off"
      >
      <span class="cdp-icon" aria-hidden="true">
        <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="4.5" width="14" height="12" rx="2"></rect>
          <line x1="3" y1="8.2" x2="17" y2="8.2"></line>
          <line x1="6.6" y1="2.6" x2="6.6" y2="5.6"></line>
          <line x1="13.4" y1="2.6" x2="13.4" y2="5.6"></line>
        </svg>
      </span>
    </div>

    <div class="cdp-panel" id="cdpPanel" role="dialog" aria-label="日付を選択" hidden>
      <header class="cdp-head">
        <button type="button" class="cdp-nav" id="cdpPrev" aria-label="前の月へ"><span aria-hidden="true">‹</span></button>
        <p class="cdp-title" id="cdpTitle" aria-live="polite">2026年7月</p>
        <button type="button" class="cdp-nav" id="cdpNext" aria-label="次の月へ"><span aria-hidden="true">›</span></button>
      </header>
      <div class="cdp-weekrow" aria-hidden="true">
        <span class="cdp-wd cdp-wd--sun">日</span><span class="cdp-wd">月</span><span class="cdp-wd">火</span><span class="cdp-wd">水</span><span class="cdp-wd">木</span><span class="cdp-wd">金</span><span class="cdp-wd cdp-wd--sat">土</span>
      </div>
      <div class="cdp-grid" id="cdpGrid" role="grid" aria-label="日付"></div>
      <button type="button" class="cdp-today" id="cdpToday">今日(2026年7月15日)を選ぶ</button>
    </div>
  </div>
  <p class="cdp-hint">入力欄をクリックするとカレンダーが開きます</p>
</div>
CSS
/* 日付ピッカー: 入力欄の直下にカレンダーカードがふわりと開く */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 60px 20px 140px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
  background:
    radial-gradient(circle at 12% 8%, rgba(79, 70, 229, .10) 0%, transparent 50%),
    radial-gradient(circle at 88% 94%, rgba(184, 134, 63, .12) 0%, transparent 50%),
    #f7f5f1;
  color: #26232c;
}

.cdp-stage { width: 100%; max-width: 320px; display: grid; gap: 10px; justify-items: center; }

.cdp-field { position: relative; width: 100%; }

.cdp-label {
  display: block;
  margin-bottom: 8px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .03em;
  color: #6b6672;
}

.cdp-control { position: relative; }

.cdp-input {
  width: 100%;
  padding: 13px 42px 13px 16px;
  font: inherit;
  font-size: 14px;
  color: #26232c;
  background: #fff;
  border: 1.4px solid #e9e5df;
  border-radius: 14px;
  cursor: pointer;
  transition: border-color .16s ease, box-shadow .16s ease;
}
.cdp-input::placeholder { color: #b6b1a8; }
.cdp-input:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }
.cdp-input[aria-expanded="true"] { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }

.cdp-icon {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: #948e9c;
  pointer-events: none;
  display: grid;
}

.cdp-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 10px);
  left: 0;
  width: 300px;
  max-width: 88vw;
  background: #fff;
  border: 1px solid #e9e5df;
  border-radius: 20px;
  padding: 18px 16px 14px;
  box-shadow: 0 30px 60px -24px rgba(38, 35, 44, .38), 0 2px 10px rgba(38, 35, 44, .06);
  transform-origin: top left;
  opacity: 0;
  transform: translateY(-6px) scale(.97);
  transition: opacity .16s ease, transform .16s ease;
}
.cdp-panel[hidden] { display: none; }
.cdp-panel.is-open { opacity: 1; transform: translateY(0) scale(1); }

.cdp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.cdp-title { margin: 0; font-size: 14.5px; font-weight: 700; color: #26232c; }

.cdp-nav {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid #ece8e1;
  border-radius: 50%;
  background: #fbfaf8;
  color: #57525e;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background .16s ease, border-color .16s ease;
}
.cdp-nav:hover { background: #f0eef0; border-color: #ddd7cd; }
.cdp-nav:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }

.cdp-weekrow { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: 4px; }
.cdp-wd { text-align: center; font-size: 10.5px; font-weight: 600; color: #a29d8f; padding-bottom: 4px; }
.cdp-wd--sun { color: #c2515f; }
.cdp-wd--sat { color: #3b82c4; }

.cdp-grid { display: grid; gap: 2px; }
.cdp-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }

.cdp-day {
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: #34303a;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
/* :not(selected)を付けないと :hover(2セレクタ分の詳細度)が --selected(1セレクタ)の背景に勝ってしまう */
.cdp-day:hover:not(.cdp-day--selected) { background: #f2f0eb; }
.cdp-day:focus-visible { outline: 2px solid #4f46e5; outline-offset: 1px; }
.cdp-day--out { color: #cac5ba; }
.cdp-day--sun { color: #c2515f; }
.cdp-day--sat { color: #3b82c4; }
.cdp-day--out.cdp-day--sun, .cdp-day--out.cdp-day--sat { color: #e3cdd1; }
.cdp-day--today { box-shadow: inset 0 0 0 1.4px #b8863f; color: #8a5e26; font-weight: 700; }
.cdp-day--selected { background: #4f46e5; color: #fff; font-weight: 700; }
.cdp-day--selected.cdp-day--today { box-shadow: inset 0 0 0 1.4px #fff; }

.cdp-today {
  display: block;
  width: 100%;
  margin-top: 12px;
  padding: 9px 10px;
  border: 1px dashed #ddd7cd;
  border-radius: 12px;
  background: #fbfaf8;
  color: #57525e;
  font-size: 12px;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.cdp-today:hover { background: #f2f0eb; border-color: #b8863f; color: #8a5e26; }
.cdp-today:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }

.cdp-hint { margin: 0; font-size: 12px; color: #a29d8f; }

@media (prefers-reduced-motion: reduce) {
  .cdp-panel, .cdp-input, .cdp-nav, .cdp-day, .cdp-today { transition: none; }
}
JavaScript
// 日付ピッカー: 入力欄クリックでカレンダーをポップし、日付選択で入力欄に反映して閉じる。外側クリックでも閉じる
(() => {
  const field = document.getElementById('cdpField');
  const input = document.getElementById('cdpInput');
  const panel = document.getElementById('cdpPanel');
  const grid = document.getElementById('cdpGrid');
  const titleEl = document.getElementById('cdpTitle');
  const prevBtn = document.getElementById('cdpPrev');
  const nextBtn = document.getElementById('cdpNext');
  const todayBtn = document.getElementById('cdpToday');
  if (!field || !input || !panel || !grid || !titleEl || !prevBtn || !nextBtn || !todayBtn) return; // null安全

  const WD = ['日', '月', '火', '水', '木', '金', '土'];
  const TODAY = { y: 2026, m: 6, d: 15 }; // 固定の「今日」= 2026年7月15日(水)

  let viewY = TODAY.y;
  let viewM = TODAY.m;
  let selected = null;

  const firstWeekday = (y, m) => new Date(y, m, 1).getDay();
  const formatDate = (y, m, d) => `${y}年${m + 1}月${d}日(${WD[new Date(y, m, d).getDay()]})`;

  function renderGrid() {
    titleEl.textContent = `${viewY}年${viewM + 1}月`;
    grid.innerHTML = '';
    const lead = firstWeekday(viewY, viewM);
    const start = new Date(viewY, viewM, 1 - lead);

    for (let row = 0; row < 6; row++) {
      const rowEl = document.createElement('div');
      rowEl.className = 'cdp-row';
      rowEl.setAttribute('role', 'row');
      for (let col = 0; col < 7; col++) {
        const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + row * 7 + col);
        const y = d.getFullYear(), m = d.getMonth(), day = d.getDate(), dow = d.getDay();
        const inMonth = m === viewM && y === viewY;
        const isToday = y === TODAY.y && m === TODAY.m && day === TODAY.d;
        const isSelected = !!selected && y === selected.y && m === selected.m && day === selected.d;

        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'cdp-day';
        if (!inMonth) btn.classList.add('cdp-day--out');
        if (dow === 0) btn.classList.add('cdp-day--sun');
        if (dow === 6) btn.classList.add('cdp-day--sat');
        if (isToday) btn.classList.add('cdp-day--today');
        if (isSelected) btn.classList.add('cdp-day--selected');
        btn.setAttribute('role', 'gridcell');
        btn.setAttribute('aria-selected', isSelected ? 'true' : 'false');
        if (isToday) btn.setAttribute('aria-current', 'date');
        btn.tabIndex = isSelected || (!selected && isToday && inMonth) ? 0 : -1;
        btn.dataset.y = String(y);
        btn.dataset.m = String(m);
        btn.dataset.d = String(day);
        btn.setAttribute('aria-label', formatDate(y, m, day));
        btn.textContent = String(day);
        rowEl.appendChild(btn);
      }
      grid.appendChild(rowEl);
    }
    if (!grid.querySelector('[tabindex="0"]')) {
      const first = grid.querySelector('.cdp-day:not(.cdp-day--out)');
      if (first) first.tabIndex = 0;
    }
  }

  function changeMonth(delta) {
    viewM += delta;
    if (viewM < 0) { viewM = 11; viewY -= 1; }
    if (viewM > 11) { viewM = 0; viewY += 1; }
    renderGrid();
  }

  const isOpen = () => !panel.hidden;

  function openPanel() {
    if (selected) { viewY = selected.y; viewM = selected.m; } else { viewY = TODAY.y; viewM = TODAY.m; }
    renderGrid();
    panel.hidden = false;
    void panel.offsetHeight; // 強制リフローで開くトランジションを確実に発火させる
    panel.classList.add('is-open');
    input.setAttribute('aria-expanded', 'true');
  }

  function closePanel(focusInput) {
    panel.hidden = true;
    panel.classList.remove('is-open');
    input.setAttribute('aria-expanded', 'false');
    if (focusInput) input.focus();
  }

  input.addEventListener('click', () => { if (isOpen()) closePanel(false); else openPanel(); });
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
      e.preventDefault();
      if (!isOpen()) openPanel();
    }
  });

  grid.addEventListener('click', (e) => {
    const btn = e.target.closest('.cdp-day');
    if (!btn) return;
    const y = Number(btn.dataset.y), m = Number(btn.dataset.m), d = Number(btn.dataset.d);
    selected = { y, m, d };
    input.value = formatDate(y, m, d);
    closePanel(true);
  });

  grid.addEventListener('keydown', (e) => {
    const btn = e.target.closest('.cdp-day');
    if (!btn) return;
    if (e.key === 'Escape') { e.preventDefault(); closePanel(true); return; }
    const map = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: 7, ArrowUp: -7 };
    const delta = map[e.key];
    if (delta === undefined) return;
    e.preventDefault();
    const all = Array.from(grid.querySelectorAll('.cdp-day'));
    const idx = all.indexOf(btn);
    const nextIdx = idx + delta;
    if (nextIdx >= 0 && nextIdx < all.length) {
      all[idx].tabIndex = -1;
      all[nextIdx].tabIndex = 0;
      all[nextIdx].focus();
    }
  });

  prevBtn.addEventListener('click', () => changeMonth(-1));
  nextBtn.addEventListener('click', () => changeMonth(1));
  todayBtn.addEventListener('click', () => {
    selected = { y: TODAY.y, m: TODAY.m, d: TODAY.d };
    input.value = formatDate(TODAY.y, TODAY.m, TODAY.d);
    closePanel(true);
  });

  document.addEventListener('click', (e) => {
    if (isOpen() && !e.target.closest('.cdp-field')) closePanel(false);
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && isOpen() && !e.target.closest('.cdp-grid')) closePanel(true);
  });

  renderGrid();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「日付ピッカー」の効果を追加してください。

# 追加してほしい効果
日付ピッカー(カレンダー & ステップ)
入力欄をクリックするとカレンダーがふわりとポップし、日付を選ぶと入力欄に反映されて閉じます。外側クリックでも自然に閉じる予約フォーム向けの部品です。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 日付ピッカー: 入力欄をクリックするとカレンダーがポップし、日付を選ぶと入力欄に反映されて閉じます -->
<div class="cdp-stage">
  <div class="cdp-field" id="cdpField">
    <label class="cdp-label" for="cdpInput">お届け希望日</label>
    <div class="cdp-control">
      <input
        type="text"
        id="cdpInput"
        class="cdp-input"
        placeholder="日付を選択してください"
        readonly
        role="combobox"
        aria-haspopup="dialog"
        aria-expanded="false"
        aria-controls="cdpPanel"
        autocomplete="off"
      >
      <span class="cdp-icon" aria-hidden="true">
        <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="4.5" width="14" height="12" rx="2"></rect>
          <line x1="3" y1="8.2" x2="17" y2="8.2"></line>
          <line x1="6.6" y1="2.6" x2="6.6" y2="5.6"></line>
          <line x1="13.4" y1="2.6" x2="13.4" y2="5.6"></line>
        </svg>
      </span>
    </div>

    <div class="cdp-panel" id="cdpPanel" role="dialog" aria-label="日付を選択" hidden>
      <header class="cdp-head">
        <button type="button" class="cdp-nav" id="cdpPrev" aria-label="前の月へ"><span aria-hidden="true">‹</span></button>
        <p class="cdp-title" id="cdpTitle" aria-live="polite">2026年7月</p>
        <button type="button" class="cdp-nav" id="cdpNext" aria-label="次の月へ"><span aria-hidden="true">›</span></button>
      </header>
      <div class="cdp-weekrow" aria-hidden="true">
        <span class="cdp-wd cdp-wd--sun">日</span><span class="cdp-wd">月</span><span class="cdp-wd">火</span><span class="cdp-wd">水</span><span class="cdp-wd">木</span><span class="cdp-wd">金</span><span class="cdp-wd cdp-wd--sat">土</span>
      </div>
      <div class="cdp-grid" id="cdpGrid" role="grid" aria-label="日付"></div>
      <button type="button" class="cdp-today" id="cdpToday">今日(2026年7月15日)を選ぶ</button>
    </div>
  </div>
  <p class="cdp-hint">入力欄をクリックするとカレンダーが開きます</p>
</div>

【CSS】
/* 日付ピッカー: 入力欄の直下にカレンダーカードがふわりと開く */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 60px 20px 140px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
  background:
    radial-gradient(circle at 12% 8%, rgba(79, 70, 229, .10) 0%, transparent 50%),
    radial-gradient(circle at 88% 94%, rgba(184, 134, 63, .12) 0%, transparent 50%),
    #f7f5f1;
  color: #26232c;
}

.cdp-stage { width: 100%; max-width: 320px; display: grid; gap: 10px; justify-items: center; }

.cdp-field { position: relative; width: 100%; }

.cdp-label {
  display: block;
  margin-bottom: 8px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .03em;
  color: #6b6672;
}

.cdp-control { position: relative; }

.cdp-input {
  width: 100%;
  padding: 13px 42px 13px 16px;
  font: inherit;
  font-size: 14px;
  color: #26232c;
  background: #fff;
  border: 1.4px solid #e9e5df;
  border-radius: 14px;
  cursor: pointer;
  transition: border-color .16s ease, box-shadow .16s ease;
}
.cdp-input::placeholder { color: #b6b1a8; }
.cdp-input:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }
.cdp-input[aria-expanded="true"] { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }

.cdp-icon {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: #948e9c;
  pointer-events: none;
  display: grid;
}

.cdp-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 10px);
  left: 0;
  width: 300px;
  max-width: 88vw;
  background: #fff;
  border: 1px solid #e9e5df;
  border-radius: 20px;
  padding: 18px 16px 14px;
  box-shadow: 0 30px 60px -24px rgba(38, 35, 44, .38), 0 2px 10px rgba(38, 35, 44, .06);
  transform-origin: top left;
  opacity: 0;
  transform: translateY(-6px) scale(.97);
  transition: opacity .16s ease, transform .16s ease;
}
.cdp-panel[hidden] { display: none; }
.cdp-panel.is-open { opacity: 1; transform: translateY(0) scale(1); }

.cdp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.cdp-title { margin: 0; font-size: 14.5px; font-weight: 700; color: #26232c; }

.cdp-nav {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid #ece8e1;
  border-radius: 50%;
  background: #fbfaf8;
  color: #57525e;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background .16s ease, border-color .16s ease;
}
.cdp-nav:hover { background: #f0eef0; border-color: #ddd7cd; }
.cdp-nav:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }

.cdp-weekrow { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: 4px; }
.cdp-wd { text-align: center; font-size: 10.5px; font-weight: 600; color: #a29d8f; padding-bottom: 4px; }
.cdp-wd--sun { color: #c2515f; }
.cdp-wd--sat { color: #3b82c4; }

.cdp-grid { display: grid; gap: 2px; }
.cdp-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }

.cdp-day {
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: #34303a;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
/* :not(selected)を付けないと :hover(2セレクタ分の詳細度)が --selected(1セレクタ)の背景に勝ってしまう */
.cdp-day:hover:not(.cdp-day--selected) { background: #f2f0eb; }
.cdp-day:focus-visible { outline: 2px solid #4f46e5; outline-offset: 1px; }
.cdp-day--out { color: #cac5ba; }
.cdp-day--sun { color: #c2515f; }
.cdp-day--sat { color: #3b82c4; }
.cdp-day--out.cdp-day--sun, .cdp-day--out.cdp-day--sat { color: #e3cdd1; }
.cdp-day--today { box-shadow: inset 0 0 0 1.4px #b8863f; color: #8a5e26; font-weight: 700; }
.cdp-day--selected { background: #4f46e5; color: #fff; font-weight: 700; }
.cdp-day--selected.cdp-day--today { box-shadow: inset 0 0 0 1.4px #fff; }

.cdp-today {
  display: block;
  width: 100%;
  margin-top: 12px;
  padding: 9px 10px;
  border: 1px dashed #ddd7cd;
  border-radius: 12px;
  background: #fbfaf8;
  color: #57525e;
  font-size: 12px;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.cdp-today:hover { background: #f2f0eb; border-color: #b8863f; color: #8a5e26; }
.cdp-today:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }

.cdp-hint { margin: 0; font-size: 12px; color: #a29d8f; }

@media (prefers-reduced-motion: reduce) {
  .cdp-panel, .cdp-input, .cdp-nav, .cdp-day, .cdp-today { transition: none; }
}

【JavaScript】
// 日付ピッカー: 入力欄クリックでカレンダーをポップし、日付選択で入力欄に反映して閉じる。外側クリックでも閉じる
(() => {
  const field = document.getElementById('cdpField');
  const input = document.getElementById('cdpInput');
  const panel = document.getElementById('cdpPanel');
  const grid = document.getElementById('cdpGrid');
  const titleEl = document.getElementById('cdpTitle');
  const prevBtn = document.getElementById('cdpPrev');
  const nextBtn = document.getElementById('cdpNext');
  const todayBtn = document.getElementById('cdpToday');
  if (!field || !input || !panel || !grid || !titleEl || !prevBtn || !nextBtn || !todayBtn) return; // null安全

  const WD = ['日', '月', '火', '水', '木', '金', '土'];
  const TODAY = { y: 2026, m: 6, d: 15 }; // 固定の「今日」= 2026年7月15日(水)

  let viewY = TODAY.y;
  let viewM = TODAY.m;
  let selected = null;

  const firstWeekday = (y, m) => new Date(y, m, 1).getDay();
  const formatDate = (y, m, d) => `${y}年${m + 1}月${d}日(${WD[new Date(y, m, d).getDay()]})`;

  function renderGrid() {
    titleEl.textContent = `${viewY}年${viewM + 1}月`;
    grid.innerHTML = '';
    const lead = firstWeekday(viewY, viewM);
    const start = new Date(viewY, viewM, 1 - lead);

    for (let row = 0; row < 6; row++) {
      const rowEl = document.createElement('div');
      rowEl.className = 'cdp-row';
      rowEl.setAttribute('role', 'row');
      for (let col = 0; col < 7; col++) {
        const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + row * 7 + col);
        const y = d.getFullYear(), m = d.getMonth(), day = d.getDate(), dow = d.getDay();
        const inMonth = m === viewM && y === viewY;
        const isToday = y === TODAY.y && m === TODAY.m && day === TODAY.d;
        const isSelected = !!selected && y === selected.y && m === selected.m && day === selected.d;

        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'cdp-day';
        if (!inMonth) btn.classList.add('cdp-day--out');
        if (dow === 0) btn.classList.add('cdp-day--sun');
        if (dow === 6) btn.classList.add('cdp-day--sat');
        if (isToday) btn.classList.add('cdp-day--today');
        if (isSelected) btn.classList.add('cdp-day--selected');
        btn.setAttribute('role', 'gridcell');
        btn.setAttribute('aria-selected', isSelected ? 'true' : 'false');
        if (isToday) btn.setAttribute('aria-current', 'date');
        btn.tabIndex = isSelected || (!selected && isToday && inMonth) ? 0 : -1;
        btn.dataset.y = String(y);
        btn.dataset.m = String(m);
        btn.dataset.d = String(day);
        btn.setAttribute('aria-label', formatDate(y, m, day));
        btn.textContent = String(day);
        rowEl.appendChild(btn);
      }
      grid.appendChild(rowEl);
    }
    if (!grid.querySelector('[tabindex="0"]')) {
      const first = grid.querySelector('.cdp-day:not(.cdp-day--out)');
      if (first) first.tabIndex = 0;
    }
  }

  function changeMonth(delta) {
    viewM += delta;
    if (viewM < 0) { viewM = 11; viewY -= 1; }
    if (viewM > 11) { viewM = 0; viewY += 1; }
    renderGrid();
  }

  const isOpen = () => !panel.hidden;

  function openPanel() {
    if (selected) { viewY = selected.y; viewM = selected.m; } else { viewY = TODAY.y; viewM = TODAY.m; }
    renderGrid();
    panel.hidden = false;
    void panel.offsetHeight; // 強制リフローで開くトランジションを確実に発火させる
    panel.classList.add('is-open');
    input.setAttribute('aria-expanded', 'true');
  }

  function closePanel(focusInput) {
    panel.hidden = true;
    panel.classList.remove('is-open');
    input.setAttribute('aria-expanded', 'false');
    if (focusInput) input.focus();
  }

  input.addEventListener('click', () => { if (isOpen()) closePanel(false); else openPanel(); });
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
      e.preventDefault();
      if (!isOpen()) openPanel();
    }
  });

  grid.addEventListener('click', (e) => {
    const btn = e.target.closest('.cdp-day');
    if (!btn) return;
    const y = Number(btn.dataset.y), m = Number(btn.dataset.m), d = Number(btn.dataset.d);
    selected = { y, m, d };
    input.value = formatDate(y, m, d);
    closePanel(true);
  });

  grid.addEventListener('keydown', (e) => {
    const btn = e.target.closest('.cdp-day');
    if (!btn) return;
    if (e.key === 'Escape') { e.preventDefault(); closePanel(true); return; }
    const map = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: 7, ArrowUp: -7 };
    const delta = map[e.key];
    if (delta === undefined) return;
    e.preventDefault();
    const all = Array.from(grid.querySelectorAll('.cdp-day'));
    const idx = all.indexOf(btn);
    const nextIdx = idx + delta;
    if (nextIdx >= 0 && nextIdx < all.length) {
      all[idx].tabIndex = -1;
      all[nextIdx].tabIndex = 0;
      all[nextIdx].focus();
    }
  });

  prevBtn.addEventListener('click', () => changeMonth(-1));
  nextBtn.addEventListener('click', () => changeMonth(1));
  todayBtn.addEventListener('click', () => {
    selected = { y: TODAY.y, m: TODAY.m, d: TODAY.d };
    input.value = formatDate(TODAY.y, TODAY.m, TODAY.d);
    closePanel(true);
  });

  document.addEventListener('click', (e) => {
    if (isOpen() && !e.target.closest('.cdp-field')) closePanel(false);
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && isOpen() && !e.target.closest('.cdp-grid')) closePanel(true);
  });

  renderGrid();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。