カレンダー(イベント・祝日対応)

日付・カレンダー 中級

このコンポーネントについて

月ごとのイベントスケジュールを表示するカレンダーUIです。 PC・タブレット(768px以上)では7列グリッドの月カレンダースマホ(768px未満)では1日1行の縦リスト表示に自動で切り替わるレスポンシブ設計です。

イベントデータをJSONで管理し、日本の祝日を外部APIから取得して土日・祝日を色分け表示します。 1日に3件以上の予定がある場合は2件まで表示して「…」で省略し、日付タップでその日のすべての予定をダイアログで確認できます。 部活動スケジュール・社内行事・施設予約など、月単位でイベントを管理する場面で活用できます。

  • レスポンシブ切り替え — 768px以上はグリッド表示、768px未満は縦リスト表示に自動切り替え
  • 月ナビゲーション — 前月・次月ボタンで月を移動。「今月」ボタンで現在月に即時戻る
  • 今日のハイライト — 今日の日付を青丸でハイライト表示する
  • 土日・祝日の色分け — 土曜は青、日曜・祝日は赤で表示。祝日名もセルに表示する
  • 前後月のグレーアウト — グリッドの端に表示される前月・次月の日付をグレーで表示する
  • イベント省略表示 — 1日3件以上は2件表示+「… 他N件」で省略する
  • イベント詳細ダイアログ — イベントあり日のタップで全予定を一覧表示。背景タップでも閉じる
  • JSONデータ管理 — title・date・startTime・endTime・location・clubの6項目を持つJSONで予定を管理する
  • 祝日API対応holidays-jp.github.io の無料APIで祝日を取得。取得失敗時はフォールバックデータで動作する

実装のポイント・注意点

祝日APIとイベントJSONは Promise.all() で並行取得します。 どちらか一方が遅くても両方揃ってからカレンダーを描画するため、データが未取得のまま表示される問題を防げます。 祝日APIは外部サービスのためネットワーク状況によって取得できない場合があります。.catch() で失敗を捕捉し、フォールバック定数(主要祝日をハードコードしたオブジェクト)に切り替える設計にしています。

fetch() はローカルの file:// プロトコルでは動きません。 VS Code の「Live Server」拡張機能や npx serve などで簡易サーバーを起動してから index.html を開いてください。

グリッドのセル数は月によって35個(5行)または42個(6行)になります。 Math.ceil((月初の曜日 + 月の日数) / 7) * 7 でセル数を算出します。 グリッドの先頭に前月末日を、末尾に次月初日を埋めてカレンダーの形を整えます。

ループ内のイベントリスナーには即時実行関数(IIFE)でクロージャを作ります。 for ループ内で addEventListener を使うと、ループ変数が共有されてすべてのセルが同じ日付を参照してしまうことがあります。 (function(ds, de){ ... })(dateStr, dayEvents) のように即時実行関数でラップして変数を束縛することで、各セルが正しい日付のイベントを参照できます。

デモ

20265

サンプルソース

4つのファイルを同じフォルダに保存し、簡易サーバーで index.html を開くと動作確認できます。
ファイル構成:index.html / style.css / script.js / data/data.jsondata フォルダを作って中に配置)
保存時の文字コードは UTF-8 を指定してください。fetch()file:// では動作しないため、VS Code の Live Server 等で開いてください。

💡 サーバーサイドのデータに差し替える場合: JS内の fetch('./data/data.json') の部分を fetch('/api/events?month=2026-05') などのAPIエンドポイントに置き換えるだけで、サーバーから取得したイベントデータを表示できます。 レスポンスが同じJSON配列形式(id / date / title / startTime / endTime / location / club)であれば、表示ロジックはそのまま流用できます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>カレンダー サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="cal-wrapper">

  <!-- カレンダーヘッダー(月ナビゲーション) -->
  <div class="cal-header">
    <div class="cal-nav">
      <button class="cal-nav-btn" id="cal-prev" aria-label="前月">&#8249;</button>
      <div class="cal-title">
        <span id="cal-year">2026</span>年
        <span id="cal-month">5</span>月
      </div>
      <button class="cal-nav-btn" id="cal-next" aria-label="次月">&#8250;</button>
    </div>
    <button class="cal-today-btn" id="cal-today">今月</button>
  </div>

  <!-- グリッドビュー(PC / タブレット:768px以上) -->
  <div class="cal-grid-view" id="cal-grid-view">
    <div class="cal-weekdays">
      <div class="cal-weekday cal-weekday--sun">日</div>
      <div class="cal-weekday">月</div>
      <div class="cal-weekday">火</div>
      <div class="cal-weekday">水</div>
      <div class="cal-weekday">木</div>
      <div class="cal-weekday">金</div>
      <div class="cal-weekday cal-weekday--sat">土</div>
    </div>
    <div class="cal-grid" id="cal-grid"><!-- JSで動的生成 --></div>
  </div>

  <!-- リストビュー(モバイル:768px未満) -->
  <div class="cal-list-view" id="cal-list-view">
    <div class="cal-list" id="cal-list"><!-- JSで動的生成 --></div>
  </div>

</div><!-- /.cal-wrapper -->

<!-- イベント詳細ダイアログ -->
<div class="cal-overlay" id="cal-overlay" aria-hidden="true">
  <div class="cal-dialog" id="cal-dialog" role="dialog" aria-modal="true" aria-labelledby="cal-dialog-title">
    <div class="cal-dialog-header">
      <h3 class="cal-dialog-title" id="cal-dialog-title"></h3>
      <button class="cal-dialog-close" id="cal-dialog-close" aria-label="閉じる">&times;</button>
    </div>
    <ul class="cal-dialog-events" id="cal-dialog-events"><!-- JSで動的生成 --></ul>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
/* === カレンダー サンプル ===
   :root の変数を書き換えると色を一括変更できます */
:root {
  --color-accent:       #2B7FE8;
  --color-accent-light: #E8F1FD;
  --color-accent-dark:  #1554A0;
  --color-border:       #D0D7E0;
  --color-text:         #1A2332;
  --color-muted:        #9AA5B4;
  --color-bg:           #F4F6F9;
  --color-danger:       #D93025;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; color: var(--color-text); padding: 24px; max-width: 960px; margin: 0 auto; }

/* ===== カレンダー外枠 ===== */
.cal-wrapper {
  background: #fff;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}

/* ===== ヘッダー ===== */
.cal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.cal-nav { display: flex; align-items: center; gap: 16px; }
.cal-nav-btn {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  width: 32px;
  height: 32px;
  font-size: 20px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s;
}
.cal-nav-btn:hover { background: var(--color-bg); }
.cal-title { font-size: 18px; font-weight: 700; min-width: 120px; text-align: center; }
.cal-today-btn {
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 5px 14px;
  font-size: 13px;
  cursor: pointer;
  background: #fff;
  transition: background 0.15s;
}
.cal-today-btn:hover { background: var(--color-bg); }

/* ===== グリッドビュー(768px以上) ===== */
.cal-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-left: 1px solid var(--color-border);
  border-top: 1px solid var(--color-border);
}
.cal-weekday {
  text-align: center;
  padding: 8px 0;
  font-size: 12px;
  font-weight: 700;
  border-right: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}
.cal-weekday--sun { color: var(--color-danger); }
.cal-weekday--sat { color: var(--color-accent); }

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-left: 1px solid var(--color-border);
}
.cal-cell {
  min-height: 100px;
  padding: 6px;
  border-right: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  overflow: hidden;
}
.cal-cell--other-month { background: #f8f8f8; }
.cal-cell--has-events { cursor: pointer; }
.cal-cell--has-events:hover { background: #f4f8ff; }

/* 日付番号 */
.cal-date-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 50%;
  margin-bottom: 2px;
}
.cal-cell--other-month .cal-date-num { color: #ccc; }
.cal-cell--sun .cal-date-num,
.cal-cell--holiday .cal-date-num { color: var(--color-danger); }
.cal-cell--sat .cal-date-num { color: var(--color-accent); }
.cal-cell--today .cal-date-num { background: var(--color-accent); color: #fff; }

/* 祝日名 */
.cal-holiday-name {
  display: block;
  font-size: 10px;
  color: var(--color-danger);
  line-height: 1.3;
  margin-bottom: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* イベントバッジ */
.cal-event {
  font-size: 11px;
  padding: 2px 5px;
  margin-bottom: 2px;
  border-radius: 3px;
  background: var(--color-accent-light);
  color: var(--color-accent-dark);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-event-more { font-size: 11px; color: var(--color-muted); padding-left: 4px; }

/* ===== リストビュー(767px以下) ===== */
.cal-list-view { display: none; }
.cal-list-row {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 10px 4px;
  border-bottom: 1px solid var(--color-border);
}
.cal-list-row--has-events { cursor: pointer; }
.cal-list-row--has-events:hover { background: #f4f8ff; }
.cal-list-date { flex-shrink: 0; width: 44px; text-align: center; }
.cal-list-date-num { font-size: 22px; font-weight: 700; line-height: 1; }
.cal-list-date-day { font-size: 11px; color: var(--color-muted); margin-top: 2px; }
.cal-list-row--today .cal-list-date-num { color: var(--color-accent); }
.cal-list-row--holiday .cal-list-date-num,
.cal-list-row--sun .cal-list-date-num { color: var(--color-danger); }
.cal-list-row--holiday .cal-list-date-day,
.cal-list-row--sun .cal-list-date-day { color: var(--color-danger); }
.cal-list-row--sat .cal-list-date-num,
.cal-list-row--sat .cal-list-date-day { color: var(--color-accent); }
.cal-list-events { flex: 1; padding-top: 4px; }
.cal-list-event {
  font-size: 13px;
  padding: 3px 8px;
  margin-bottom: 3px;
  border-radius: 3px;
  background: var(--color-accent-light);
  color: var(--color-accent-dark);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-list-event-more { font-size: 12px; color: var(--color-muted); padding-left: 4px; }
.cal-list-no-events { font-size: 13px; color: var(--color-muted); padding-top: 4px; }

/* ===== ダイアログ ===== */
.cal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}
.cal-overlay.is-open { opacity: 1; pointer-events: auto; }
.cal-dialog {
  background: #fff;
  border-radius: 10px;
  width: 90%;
  max-width: 440px;
  max-height: 80vh;
  overflow-y: auto;
  padding: 20px 24px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.18);
}
.cal-dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--color-border);
}
.cal-dialog-title { font-size: 17px; font-weight: 700; }
.cal-dialog-close {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: var(--color-muted);
  line-height: 1;
  padding: 0 4px;
}
.cal-dialog-close:hover { color: var(--color-text); }
.cal-dialog-events { list-style: none; padding: 0; margin: 0; }
.cal-dialog-event { padding: 12px 0; border-bottom: 1px solid var(--color-border); }
.cal-dialog-event:last-child { border-bottom: none; }
.cal-dialog-event-title { font-size: 15px; font-weight: 600; margin-bottom: 5px; }
.cal-dialog-event-meta { font-size: 13px; color: var(--color-muted); line-height: 1.7; }

/* ===== レスポンシブ ===== */
@media (max-width: 767px) {
  .cal-grid-view { display: none; }
  .cal-list-view { display: block; }
}
// 状態管理
var currentYear = 2026;
var currentMonth = 5;
var holidays = {};
var events = [];
var DAYS_JA = ['日', '月', '火', '水', '木', '金', '土'];

// 祝日APIが取得できない場合のフォールバック(2026年主要祝日)
var FALLBACK_HOLIDAYS = {
  '2026-01-01': '元日',     '2026-01-12': '成人の日',
  '2026-02-11': '建国記念の日', '2026-02-23': '天皇誕生日',
  '2026-03-20': '春分の日',  '2026-04-29': '昭和の日',
  '2026-05-03': '憲法記念日', '2026-05-04': 'みどりの日',
  '2026-05-05': 'こどもの日', '2026-05-06': '振替休日',
  '2026-07-20': '海の日',   '2026-08-11': '山の日',
  '2026-09-21': '敬老の日',  '2026-09-23': '秋分の日',
  '2026-10-12': 'スポーツの日','2026-11-03': '文化の日',
  '2026-11-23': '勤労感謝の日'
};

// 祝日APIとイベントJSONを並行取得する
// ※ fetch() は file:// では動作しないため、簡易サーバーで開いてください
Promise.all([
  fetch('https://holidays-jp.github.io/api/v1/date.json')
    .then(function (r) { return r.json(); })
    .catch(function () { return null; }),
  // fetch('/api/events') などサーバーAPIに置き換えることもできます
  fetch('./data/data.json')
    .then(function (r) { return r.json(); })
]).then(function (results) {
  holidays = results[0] || FALLBACK_HOLIDAYS;
  events   = results[1];
  renderCalendar(currentYear, currentMonth);
  // モバイルリスト:5月20日を初期スクロール位置にする
  setTimeout(function () {
    var target = document.getElementById('cal-row-2026-05-20');
    if (target) target.scrollIntoView({ block: 'start' });
  }, 100);
}).catch(function (err) {
  console.error('データ読み込みエラー:', err);
});

// 前月ボタン
document.getElementById('cal-prev').addEventListener('click', function () {
  currentMonth--;
  if (currentMonth < 1) { currentMonth = 12; currentYear--; }
  renderCalendar(currentYear, currentMonth);
});

// 次月ボタン
document.getElementById('cal-next').addEventListener('click', function () {
  currentMonth++;
  if (currentMonth > 12) { currentMonth = 1; currentYear++; }
  renderCalendar(currentYear, currentMonth);
});

// 今月ボタン(現在の実際の月に戻る)
document.getElementById('cal-today').addEventListener('click', function () {
  var today = new Date();
  currentYear  = today.getFullYear();
  currentMonth = today.getMonth() + 1;
  renderCalendar(currentYear, currentMonth);
});

// カレンダー全体を再描画する
function renderCalendar(year, month) {
  document.getElementById('cal-year').textContent  = year;
  document.getElementById('cal-month').textContent = month;
  renderGrid(year, month);
  renderList(year, month);
}

// 日付文字列(YYYY-MM-DD)を生成する
function toDateStr(year, month, day) {
  return year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
}

// 月の日数を返す
function getDaysInMonth(year, month) { return new Date(year, month, 0).getDate(); }

// 今日の日付かどうか判定する
function isToday(year, month, day) {
  var t = new Date();
  return t.getFullYear() === year && t.getMonth() + 1 === month && t.getDate() === day;
}

// 指定日のイベント配列を返す
function getEventsForDate(dateStr) {
  return events.filter(function (e) { return e.date === dateStr; });
}

// グリッドビューを描画する(PC / タブレット)
function renderGrid(year, month) {
  var grid     = document.getElementById('cal-grid');
  grid.innerHTML = '';
  var firstDow  = new Date(year, month - 1, 1).getDay();
  var daysIn    = getDaysInMonth(year, month);
  var prevMonth = month === 1 ? 12 : month - 1;
  var prevYear  = month === 1 ? year - 1 : year;
  var prevDays  = getDaysInMonth(prevYear, prevMonth);
  // 5行 or 6行になるよう必要なセル数を算出する
  var total = Math.ceil((firstDow + daysIn) / 7) * 7;

  for (var i = 0; i < total; i++) {
    var cell = document.createElement('div');
    cell.className = 'cal-cell';
    var day, cy = year, cm = month, isOther = false;

    if (i < firstDow) {
      // 前月の日付
      day = prevDays - firstDow + i + 1; cm = prevMonth; cy = prevYear; isOther = true;
    } else if (i >= firstDow + daysIn) {
      // 次月の日付
      day = i - firstDow - daysIn + 1;
      cm  = month === 12 ? 1 : month + 1;
      cy  = month === 12 ? year + 1 : year;
      isOther = true;
    } else {
      day = i - firstDow + 1;
    }

    var dow = i % 7;
    var ds  = toDateStr(cy, cm, day);
    var isH = !isOther && !!holidays[ds];

    if (isOther) cell.classList.add('cal-cell--other-month');
    if (isH)     cell.classList.add('cal-cell--holiday');
    if (dow === 0 || isH)  cell.classList.add('cal-cell--sun');
    else if (dow === 6)    cell.classList.add('cal-cell--sat');
    if (!isOther && isToday(year, month, day)) cell.classList.add('cal-cell--today');

    // 日付番号
    var numEl = document.createElement('span');
    numEl.className = 'cal-date-num';
    numEl.textContent = day;
    cell.appendChild(numEl);

    // 祝日名ラベル
    if (isH) {
      var hEl = document.createElement('span');
      hEl.className   = 'cal-holiday-name';
      hEl.textContent = holidays[ds];
      cell.appendChild(hEl);
    }

    // イベント表示(当月のみ)
    if (!isOther) {
      var de = getEventsForDate(ds);
      if (de.length > 0) {
        cell.classList.add('cal-cell--has-events');
        // 最大2件表示する
        var shown = Math.min(de.length, 2);
        for (var j = 0; j < shown; j++) {
          var evEl = document.createElement('div');
          evEl.className   = 'cal-event';
          evEl.textContent = de[j].title;
          cell.appendChild(evEl);
        }
        // 3件以上は「… 他N件」で省略する
        if (de.length > 2) {
          var mEl = document.createElement('div');
          mEl.className   = 'cal-event-more';
          mEl.textContent = '… 他' + (de.length - 2) + '件';
          cell.appendChild(mEl);
        }
        // クロージャでdateStr・eventsを束縛してからクリックイベントを付ける
        (function (s, ev) {
          cell.addEventListener('click', function () { showDialog(s, ev); });
        })(ds, de);
      }
    }
    grid.appendChild(cell);
  }
}

// リストビューを描画する(モバイル)
function renderList(year, month) {
  var list = document.getElementById('cal-list');
  list.innerHTML = '';
  var daysIn = getDaysInMonth(year, month);

  for (var d = 1; d <= daysIn; d++) {
    var ds  = toDateStr(year, month, d);
    var dow = new Date(year, month - 1, d).getDay();
    var isH = !!holidays[ds];
    var de  = getEventsForDate(ds);

    var row = document.createElement('div');
    row.className = 'cal-list-row';
    row.id = 'cal-row-' + ds;

    if (isToday(year, month, d))  row.classList.add('cal-list-row--today');
    if (isH)                      row.classList.add('cal-list-row--holiday');
    else if (dow === 0)            row.classList.add('cal-list-row--sun');
    else if (dow === 6)            row.classList.add('cal-list-row--sat');
    if (de.length > 0)            row.classList.add('cal-list-row--has-events');

    // 日付部分
    var dp  = document.createElement('div'); dp.className = 'cal-list-date';
    var dn  = document.createElement('div'); dn.className = 'cal-list-date-num'; dn.textContent = d;
    var dd  = document.createElement('div'); dd.className = 'cal-list-date-day';
    dd.textContent = isH ? '祝' : DAYS_JA[dow];
    dp.appendChild(dn); dp.appendChild(dd); row.appendChild(dp);

    // イベント部分
    var ep = document.createElement('div'); ep.className = 'cal-list-events';
    if (de.length === 0) {
      var noEv = document.createElement('p'); noEv.className = 'cal-list-no-events';
      noEv.textContent = '予定なし'; ep.appendChild(noEv);
    } else {
      var shown = Math.min(de.length, 2);
      for (var j = 0; j < shown; j++) {
        var evEl = document.createElement('div'); evEl.className = 'cal-list-event';
        evEl.textContent = de[j].title; ep.appendChild(evEl);
      }
      if (de.length > 2) {
        var mEl = document.createElement('div'); mEl.className = 'cal-list-event-more';
        mEl.textContent = '… 他' + (de.length - 2) + '件'; ep.appendChild(mEl);
      }
      (function (s, ev) {
        row.addEventListener('click', function () { showDialog(s, ev); });
      })(ds, de);
    }
    row.appendChild(ep);
    list.appendChild(row);
  }
}

// ダイアログを表示する
function showDialog(dateStr, dayEvents) {
  var p   = dateStr.split('-');
  var m   = parseInt(p[1]), d = parseInt(p[2]);
  var dow = new Date(parseInt(p[0]), m - 1, d).getDay();
  document.getElementById('cal-dialog-title').textContent = m + '月' + d + '日(' + DAYS_JA[dow] + ')';

  var ul = document.getElementById('cal-dialog-events');
  ul.innerHTML = '';
  dayEvents.forEach(function (ev) {
    var li    = document.createElement('li'); li.className = 'cal-dialog-event';
    var title = document.createElement('div'); title.className = 'cal-dialog-event-title'; title.textContent = ev.title;
    var meta  = document.createElement('div'); meta.className  = 'cal-dialog-event-meta';
    meta.textContent = ev.startTime + '〜' + ev.endTime + ' ' + ev.location + ' ' + ev.club;
    li.appendChild(title); li.appendChild(meta); ul.appendChild(li);
  });

  var ov = document.getElementById('cal-overlay');
  ov.classList.add('is-open');
  ov.setAttribute('aria-hidden', 'false');
}

// ダイアログを閉じる
function closeDialog() {
  var ov = document.getElementById('cal-overlay');
  ov.classList.remove('is-open');
  ov.setAttribute('aria-hidden', 'true');
}
document.getElementById('cal-dialog-close').addEventListener('click', closeDialog);
// オーバーレイ(背景)タップで閉じる。ダイアログ本体のクリックは伝播させない
document.getElementById('cal-overlay').addEventListener('click', function (e) {
  if (e.target === this) closeDialog();
});

data フォルダを作成し、data.json として保存してください。

[
  { "id": 1,  "date": "2026-05-15", "title": "定期練習",           "startTime": "16:00", "endTime": "18:00", "location": "グラウンド",      "club": "サッカー部" },
  { "id": 2,  "date": "2026-05-16", "title": "練習試合",           "startTime": "09:00", "endTime": "13:00", "location": "第1グラウンド",    "club": "野球部" },
  { "id": 3,  "date": "2026-05-17", "title": "春季大会(1日目)",   "startTime": "09:00", "endTime": "17:00", "location": "市民体育館",       "club": "バスケ部" },
  { "id": 4,  "date": "2026-05-18", "title": "合同練習",           "startTime": "15:00", "endTime": "17:30", "location": "グラウンド",       "club": "陸上部" },
  { "id": 5,  "date": "2026-05-18", "title": "定期練習",           "startTime": "16:00", "endTime": "18:30", "location": "体育館",           "club": "バレー部" },
  { "id": 6,  "date": "2026-05-19", "title": "インターハイ地区予選", "startTime": "09:00", "endTime": "17:00", "location": "県立陸上競技場",   "club": "陸上部" },
  { "id": 7,  "date": "2026-05-19", "title": "春季練習試合",        "startTime": "13:00", "endTime": "16:00", "location": "テニスコート",      "club": "テニス部" },
  { "id": 8,  "date": "2026-05-20", "title": "地区予選",           "startTime": "10:00", "endTime": "16:00", "location": "○○スタジアム",    "club": "サッカー部" },
  { "id": 9,  "date": "2026-05-20", "title": "楽器メンテナンス講習", "startTime": "14:00", "endTime": "16:00", "location": "音楽室",           "club": "吹奏楽部" },
  { "id": 10, "date": "2026-05-20", "title": "新入部員歓迎試合",    "startTime": "09:00", "endTime": "12:00", "location": "体育館",           "club": "バスケ部" },
  { "id": 11, "date": "2026-05-21", "title": "定期演奏練習",       "startTime": "15:00", "endTime": "18:00", "location": "音楽室",           "club": "吹奏楽部" },
  { "id": 12, "date": "2026-05-21", "title": "新入部員歓迎ゲーム",  "startTime": "14:00", "endTime": "16:00", "location": "テニスコート",      "club": "テニス部" },
  { "id": 13, "date": "2026-05-22", "title": "合宿説明会",         "startTime": "16:00", "endTime": "17:30", "location": "会議室",           "club": "サッカー部" },
  { "id": 14, "date": "2026-05-23", "title": "春季発表会準備",      "startTime": "13:00", "endTime": "17:00", "location": "音楽室",           "club": "吹奏楽部" },
  { "id": 15, "date": "2026-05-23", "title": "練習試合",           "startTime": "09:00", "endTime": "13:00", "location": "第2グラウンド",    "club": "野球部" },
  { "id": 16, "date": "2026-05-23", "title": "体力測定",           "startTime": "14:00", "endTime": "16:00", "location": "グラウンド",       "club": "陸上部" },
  { "id": 17, "date": "2026-05-24", "title": "春季大会(2日目)",   "startTime": "09:00", "endTime": "17:00", "location": "市民体育館",       "club": "バスケ部" },
  { "id": 18, "date": "2026-05-24", "title": "定期練習",           "startTime": "14:00", "endTime": "17:00", "location": "グラウンド",       "club": "陸上部" },
  { "id": 19, "date": "2026-05-25", "title": "定期練習(試合前調整)","startTime": "16:00", "endTime": "18:00", "location": "体育館",           "club": "バレー部" }
]

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。イベントデータの変更や色の調整など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# カレンダー(イベント・祝日対応)作成依頼

## 概要
部活動スケジュールなどのイベントデータをJSONから読み込み、月カレンダーに表示するUIを実装してください。
PC/タブレットはグリッド表示、スマホは縦リスト表示に切り替わるレスポンシブ対応とします。

## 要件
- PC・タブレット(768px以上): 7列グリッドの月カレンダー表示
- スマホ(768px未満): 1日1行の縦リスト表示
- 前月・次月ボタンで月を移動できる。「今月」ボタンで現在月に戻る
- 今日の日付をハイライト表示する
- 土曜は青、日曜・祝日は赤でテキストを色分けする
- 祝日はAPIから取得する。API: https://holidays-jp.github.io/api/v1/date.json(形式: { "YYYY-MM-DD": "祝日名" })
- API取得失敗時はハードコードしたフォールバックデータで動作する
- グリッド表示で前月・次月のはみ出し日はグレーアウト表示する
- イベントデータはJSONから取得する(フィールド: id, date, title, startTime, endTime, location, club)
- 1日の表示は最大2件。3件以上の場合は「… 他N件」で省略する(グリッド・リスト共通)
- イベントがある日をタップするとダイアログを表示し、その日の全イベントを一覧表示する
- ダイアログヘッダーには「M月D日(曜)」を表示する
- ダイアログは閉じるボタン(×)またはダイアログ外タップで閉じる
- イベントがない日はタップしても何もしない
- スマホのリスト表示では当月全日を表示し、5月20日を初期スクロール位置にする(デモ固定月: 2026年5月)

## JSONデータ仕様
ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)
各イベントオブジェクトの構造:
{ "id": 数値, "date": "YYYY-MM-DD", "title": "イベント名", "startTime": "HH:MM", "endTime": "HH:MM", "location": "場所名", "club": "部活名" }

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(768px でグリッド↔リスト切り替え)

## 動作詳細
- ページロード時に Promise.all で祝日APIとイベントJSONを並行取得する
- グリッドは月初の曜日に合わせて先頭に空セルを配置し、前月末日でパディングする
- セルの高さは固定(min-height: 100px 程度)でイベントが収まらない場合は省略表示する
- リスト表示では data-id 属性などで各行を特定し、scrollIntoView で初期位置を設定する
- 祝日APIのレスポンスをオブジェクトとして保持し、日付文字列(YYYY-MM-DD)をキーに祝日名を取得する
- ループ内のクリックイベントは即時実行関数(IIFE)でクロージャを作り変数を束縛する

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
JSONファイル(data/data.json)も合わせて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。