「自分で使えるカレンダーを作ってみたいけれど、難しそう…」と感じたことはありませんか?
実は、HTMLとJavaScriptだけで、複数日にわたるイベントや色分け対応までできる本格的なカレンダーWebツールを作ることができます。しかも、コードをそのままコピペすれば、誰でもすぐに動かせるんです。
この記事では、「複数日イベント対応・色分け管理・PNG保存可」 のカレンダー作成Webツールを例に、完成コードから実装の仕組みまで丁寧に解説します。
プログラミング初心者でも「まずは動かしてみる」ことから始められる内容になっているので安心です。
ぜひこの記事を参考に、あなただけのオリジナルカレンダーツールを作ってみてください。
【実際のカレンダー作成ツールの動作を試してみたい方はこちらからどうぞ】▼
このチュートリアルで作るカレンダーツールの特徴
今回作成するカレンダーツールは、ただの日付表示だけではなく、実際に「イベントの管理」に役立つように作られています。
市販アプリほど複雑ではありませんが、Webブラウザ上でシンプルに動く実用的な仕組みを学ぶことができます。
主な特徴
- ブラウザで完結
インストールやログインは不要。HTMLファイルを開くだけで動作します。 - 複数日イベントに対応
1日だけの予定ではなく、数日間続くイベントも横長の帯として表示できます。
旅行や連休の予定管理に便利です。 - 色分け管理が可能
イベントごとに色を変えられるので、仕事・プライベート・学習などカテゴリごとに視覚的に整理できます。 - ローカル保存と削除が選べる
入れた予定はブラウザに保存できるため、次回アクセス時にも残ります。もちろん、不要な場合は削除も可能です。 - PNG画像として書き出せる
作成したカレンダーをそのままPNG画像に変換してダウンロードできます。
友人やチームとの共有にも便利です。
このツールは“ただ表示するカレンダー”ではなく、“実際にイベントを管理できるカレンダー”です。
複数日イベントや色分け機能まで備わっているので、作ってみるだけでも大きな学びになりますよ。
完成コード全文|コピペで動かしてみよう
下記をそのまま1ファイル(例:calendar.html)として保存し、ダブルクリックでブラウザで開けば動きます。
インストール不要・サーバー不要の単一HTMLです。
手順(超かんたん)
- 下のコードをコピー → テキストエディタに貼り付け
calendar.htmlという名前で保存(UTF-8)- 保存したファイルをブラウザ(Chrome/Edge等)で開く
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>シンプル月間カレンダー|ローカル保存(任意) & PNG書き出し</title> <style> :root{ --bg:#f7f7f8; --card:#fff; --line:#e5e7eb; --text:#111827; --muted:#6b7280; --c1:#2563eb; --c2:#10b981; --c3:#f59e0b; --c4:#ef4444; --c5:#8b5cf6; --cell-base: 74px; } *{ box-sizing:border-box; } body{ margin:0; background:var(--bg); color:var(--text); font-family: system-ui,-apple-system,Segoe UI,Roboto,"Hiragino Kaku Gothic ProN",Meiryo,sans-serif;} .wrap{ max-width: 960px; margin: 24px auto; padding: 0 16px; } .card{ background:var(--card); border:1px solid var(--line); border-radius: 12px; padding: 12px; } .header{ display:flex; align-items:center; gap:8px; } .title{ font-weight:700; font-size: clamp(18px, 2.8vw, 20px); padding:8px 12px; } .btn{ cursor:pointer; border:1px solid var(--line); background:#f3f4f6; color:#111827; padding:8px 12px; border-radius:8px; } .btn:hover{ background:#e5e7eb; } .btn.primary{ background:#e8efff; color:#1e40af; border-color:#bfd3ff; } .btn.red{ background:#fee2e2; border-color:#fecaca; color:#991b1b; } .lead{ color:var(--muted); margin: 8px 0 12px; } .toolbar{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; } .calendar{ margin-top: 10px; } .weekdays{ display:grid; grid-template-columns: repeat(7, 1fr); gap: 4px; color:var(--muted); font-weight: 700; font-size: 13px; } .weekdays div{ text-align:center; padding:6px 0; } .weekdays .sun{ color:#ef4444; } .weekdays .sat{ color:#2563eb; } .weeks{ display:flex; flex-direction:column; gap:4px; } .week{ position:relative; display:grid; grid-template-columns: repeat(7, 1fr); gap:4px; } .cell{ background:#fff; border:1px solid var(--line); border-radius:10px; padding:6px 6px 6px 8px; min-height: var(--cell-base); position:relative; overflow:hidden; } .cell .date{ font-size:12px; font-weight:600; color:#374151; } .cell.sun .date{ color:#ef4444; } .cell.sat .date{ color:#2563eb; } .cell.out{ color:#9ca3af; background:#fafafa; } .tracks{ position:absolute; left:0; right:0; top:26px; pointer-events:none; } .bar{ position:absolute; height:22px; border-radius:12px; color:#fff; font-size:12px; display:flex; align-items:center; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; padding:0 8px; border:1px solid rgba(255,255,255,.6); box-shadow:0 1px 0 rgba(0,0,0,.06) inset; cursor:pointer; pointer-events:auto; } .c1{ background:var(--c1);} .c2{ background:var(--c2);} .c3{ background:var(--c3);} .c4{ background:var(--c4);} .c5{ background:var(--c5);} .legend{ display:flex; gap:6px; align-items:center; font-size:12px; color:var(--muted); } .chip{ width:16px; height:16px; border-radius:4px; display:inline-block; border:1px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,.05) inset; } .chip.c1{ background:var(--c1);} .chip.c2{background:var(--c2);} .chip.c3{background:var(--c3);} .chip.c4{background:var(--c4);} .chip.c5{background:var(--c5);} .modal{ position:fixed; inset:0; background: rgba(0,0,0,.45); display:none; align-items:center; justify-content:center; padding:16px; } .dialog{ width:min(560px, 96vw); background:#fff; border-radius:12px; border:1px solid var(--line); padding:16px; } .dialog h3{ margin:0 0 8px; font-size:18px; } .form{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; } .form .full{ grid-column: 1 / -1; } .input{ height:36px; border:1px solid #d1d5db; border-radius:8px; padding:6px 8px; width:100%; } textarea.input{ height:auto; min-height:56px; resize:vertical; } .actions{ display:flex; gap:8px; justify-content:flex-end; margin-top:10px; } .color-pick{ display:flex; gap:8px; align-items:center; } .color-btn{ width:28px; height:28px; border-radius:7px; border:2px solid transparent; cursor:pointer; box-shadow:0 0 0 1px rgba(0,0,0,.08) inset; } .color-btn.active{ outline:2px solid #111827; } .color-btn.c1{ background:var(--c1);} .color-btn.c2{background:var(--c2);} .color-btn.c3{ background:var(--c3);} .color-btn.c4{background:var(--c4);} .color-btn.c5{background:var(--c5);} /* PNG書き出し時に非表示にする要素(ボタン類のみ) */ .no-export{} .exporting .no-export{ display:none !important; } @media (max-width: 720px){ .form{ grid-template-columns: 1fr; } } @media print{ .no-export{ display:none !important; } .wrap{ max-width:none; margin:0; } .cell{ break-inside:avoid; } } </style> </head> <body> <div class="wrap" id="captureArea"> <!-- ヘッダーは表示のまま。矢印とツール群だけ no-export --> <div class="card header" aria-label="ヘッダー"> <button class="btn no-export" id="prevBtn" aria-label="前の月">‹</button> <div class="title" id="title">年月</div> <button class="btn no-export" id="nextBtn" aria-label="次の月">›</button> <div class="toolbar no-export" style="margin-left:auto; gap:6px;"> <button class="btn primary" id="exportBtn">PNG書き出し</button> <button class="btn" id="saveBtn" title="この端末に保存">保存(この端末)</button> <button class="btn red" id="clearBtn" title="保存データ削除">保存データ削除</button> <button class="btn" id="todayBtn">今日</button> </div> </div> <p class="lead no-export">セルや帯をクリックすると、イベントを追加・編集できます。保存は任意でローカルストレージにのみ行われます(サーバー送信なし)。</p> <div class="calendar card" id="calendar" aria-label="月間カレンダー"> <div class="weekdays"> <div class="sun">日</div><div>月</div><div>火</div><div>水</div><div>木</div><div>金</div><div class="sat">土</div> </div> <div id="weeks" class="weeks"></div> </div> <p class="lead no-export" id="msg" style="font-size:12px; color:#6b7280; margin-top:8px;"></p> </div> <!-- モーダル(追加・編集) --> <div class="modal" id="modal" aria-hidden="true"> <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="dlgTitle"> <h3 id="dlgTitle">イベント</h3> <div class="form"> <div class="full"> <label>タイトル</label> <input id="mTitle" class="input" maxlength="30" placeholder="例:行事・イベント名"> </div> <div> <label>開始日</label> <input id="mStart" type="date" class="input"> </div> <div> <label>終了日</label> <input id="mEnd" type="date" class="input"> </div> <div class="full"> <label>色</label> <div id="colorPick" class="color-pick"> <button class="color-btn c1" data-color="c1" aria-label="青"></button> <button class="color-btn c2" data-color="c2" aria-label="緑"></button> <button class="color-btn c3" data-color="c3" aria-label="橙"></button> <button class="color-btn c4" data-color="c4" aria-label="赤"></button> <button class="color-btn c5" data-color="c5" aria-label="紫"></button> </div> </div> <div class="full"> <label>メモ(任意・60文字まで)</label> <textarea id="mNote" class="input" maxlength="60" placeholder="任意のメモ(最大60文字)"></textarea> </div> </div> <div class="actions"> <button class="btn red" id="delBtn">削除</button> <button class="btn" id="cancelBtn">キャンセル</button> <button class="btn primary" id="saveEventBtn">登録</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <script> const pad = n => n.toString().padStart(2,'0'); const fmt = d => d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()); const parse = s => { const [y,m,d]=s.split('-').map(Number); return new Date(y, m-1, d); }; const daysBetween = (a,b)=> Math.round((b - a)/86400000); const notify = (t) => { const el = document.getElementById('msg'); el.textContent = t; if(t) setTimeout(()=> el.textContent='', 2200); }; const KEY='uys-monthcal-v1'; let EVENTS = []; try{ const raw = localStorage.getItem(KEY); if(raw){ EVENTS = JSON.parse(raw)||[]; } }catch{} function saveToStorage(){ try{ localStorage.setItem(KEY, JSON.stringify(EVENTS)); notify('保存しました(この端末のブラウザのみ)'); }catch{ alert('保存に失敗しました'); } } function clearStorage(){ try{ localStorage.removeItem(KEY); notify('保存データを削除しました'); }catch{ alert('削除に失敗しました'); } } let current = new Date(); current.setDate(1); const $ = s => document.querySelector(s); const weeksEl = $('#weeks'), titleEl = $('#title'); function render(){ titleEl.textContent = current.getFullYear() + '年' + (current.getMonth()+1) + '月'; const y=current.getFullYear(), m=current.getMonth(); const first=new Date(y,m,1), last=new Date(y,m+1,0); const start=new Date(first); start.setDate(first.getDate()-first.getDay()); const end=new Date(last); end.setDate(last.getDate()+(6-last.getDay())); weeksEl.innerHTML=''; const days=[]; const ptr=new Date(start); while(ptr<=end){ days.push(new Date(ptr)); ptr.setDate(ptr.getDate()+1); } for(let i=0;i<days.length;i+=7){ const wk=days.slice(i,i+7); const week=document.createElement('div'); week.className='week'; const cellRefs=[]; wk.forEach(d=>{ const cell=document.createElement('div'); const dow=d.getDay(); cell.className='cell'+(d.getMonth()!==m?' out':'')+(dow===0?' sun':'')+(dow===6?' sat':''); cell.setAttribute('role','button'); cell.setAttribute('tabindex','0'); cell.innerHTML=`<div class="date">${d.getDate()}</div>`; cell.addEventListener('click',()=>openModal({start:fmt(d),end:fmt(d)})); cellRefs.push(cell); week.appendChild(cell); }); const tracks=document.createElement('div'); tracks.className='tracks'; week.appendChild(tracks); const wkStart=wk[0], wkEnd=wk[6]; const slice=EVENTS.map(e=>({...e,s:parse(e.start),e2:parse(e.end)})) .filter(e=> e.s<=wkEnd && e.e2>=wkStart) .sort((a,b)=> a.s-b.s || a.e2-b.e2); const lanes=[]; const bars=[]; for(const ev of slice){ const colStart=Math.max(1, daysBetween(wkStart,ev.s)+1); const colEnd=Math.min(7, daysBetween(wkStart,ev.e2)+1); let lane=0; while(true){ if(!lanes[lane] || lanes[lane] < colStart){ lanes[lane]=colEnd; break; } lane++; } bars.push({ev, colStart, colEnd, lane}); } const laneH=24; const extra = lanes.length ? (lanes.length*laneH+6) : 0; cellRefs.forEach(c=> c.style.minHeight=`calc(var(--cell-base) + ${extra}px)`); tracks.style.height = (extra? extra : 0) + 'px'; bars.forEach(b=>{ const div=document.createElement('div'); div.className='bar '+(b.ev.color||'c1'); const leftPct=((b.colStart-1)/7*100); const rightPct=(1-(b.colEnd/7))*100; div.style.left=`calc(${leftPct}% + 2px)`; div.style.right=`calc(${rightPct}% + 2px)`; div.style.top=(b.lane*laneH)+'px'; div.title=`${b.ev.title}(${b.ev.start}〜${b.ev.end})`; div.textContent=b.ev.title; div.addEventListener('click',(e)=>{ e.stopPropagation(); openModal(b.ev); }); tracks.appendChild(div); }); weeksEl.appendChild(week); } } const modal=$('#modal'); let editingId=null; let colorSelected='c1'; [...document.querySelectorAll('.color-btn')].forEach(btn=>{ btn.addEventListener('click', ()=>{ colorSelected=btn.dataset.color; document.querySelectorAll('.color-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }); }); function setColorActive(c){ colorSelected=c||'c1'; document.querySelectorAll('.color-btn').forEach(b=> b.classList.toggle('active', b.dataset.color===colorSelected)); } function openModal(ev){ editingId=ev.id||null; $('#dlgTitle').textContent=editingId?'イベントを編集':'イベントを追加'; $('#mTitle').value=ev.title||''; $('#mStart').value=ev.start||fmt(new Date()); $('#mEnd').value=ev.end||(ev.start||fmt(new Date())); $('#mNote').value=ev.note||''; setColorActive(ev.color||'c1'); $('#delBtn').style.display=editingId?'inline-block':'none'; modal.style.display='flex'; modal.setAttribute('aria-hidden','false'); setTimeout(()=>$('#mTitle').focus(),30); } function closeModal(){ modal.style.display='none'; modal.setAttribute('aria-hidden','true'); editingId=null; } $('#cancelBtn').addEventListener('click', closeModal); modal.addEventListener('click', e=>{ if(e.target.id==='modal') closeModal(); }); $('#saveEventBtn').addEventListener('click', ()=>{ const title=$('#mTitle').value.trim(); const start=$('#mStart').value; const end=$('#mEnd').value||start; const note=$('#mNote').value.trim(); if(title.length<1){ alert('タイトルを入力してください'); return; } if(!start){ alert('開始日を入力してください'); return; } let s=parse(start), e=parse(end); if(e<s) e=s; const obj={ id: editingId || (crypto.randomUUID?crypto.randomUUID():('e'+Math.random().toString(36).slice(2))), title, start:fmt(s), end:fmt(e), color:colorSelected, note }; EVENTS = editingId ? EVENTS.map(x=>x.id===editingId?obj:x) : [...EVENTS, obj]; closeModal(); render(); notify('イベントを登録しました(端末保存は「保存」から)'); }); $('#delBtn').addEventListener('click', ()=>{ if(!editingId) return; if(confirm('このイベントを削除しますか?')){ EVENTS = EVENTS.filter(x=>x.id!==editingId); closeModal(); render(); notify('イベントを削除しました(端末保存は「保存」から)'); } }); $('#prevBtn').addEventListener('click', ()=>{ current.setMonth(current.getMonth()-1); render(); }); $('#nextBtn').addEventListener('click', ()=>{ current.setMonth(current.getMonth()+1); render(); }); $('#todayBtn').addEventListener('click', ()=>{ current=new Date(); current.setDate(1); render(); }); // PNG:ボタン類だけ隠す → タイトルは残る $('#exportBtn').addEventListener('click', async ()=>{ try{ closeModal(); document.body.classList.add('exporting'); const node=document.getElementById('captureArea'); const canvas=await html2canvas(node,{ backgroundColor:'#ffffff', scale:2 }); document.body.classList.remove('exporting'); const a=document.createElement('a'); a.href=canvas.toDataURL('image/png'); a.download='calendar.png'; a.click(); }catch(e){ document.body.classList.remove('exporting'); console.error(e); alert('画像の書き出しに失敗しました'); } }); $('#saveBtn').addEventListener('click', saveToStorage); $('#clearBtn').addEventListener('click', ()=>{ if(confirm('保存データを削除しますか?(現在の表示は消えません)')) clearStorage(); }); render(); </script> </body> </html> |
まずは“動くもの”を体験しよう。次の章で、HTML・CSS・JavaScriptのポイントをやさしく解説していくよ!
コード解説(HTML・CSSの要点)
「まずは動かす → 仕組みを理解」の順で学ぶと吸収が早いです。ここでは完成コードの“カギ”になる部分だけを、初心者にもわかりやすく要点解説します。
HTML:骨組みと役割の整理
- ヘッダー(.header)
年月の見出し(#title)と、月移動ボタン(#prevBtn/#nextBtn)、右側のツール群(PNG書き出し・保存・削除・今日へ)の配置。 - カレンダー本体(.calendar)
- 曜日行(
.weekdays)… 日曜は赤、土曜は青にする土台。 - 週の集合(
#weeks)… JavaScriptで週を順に生成して差し込みます。
- 曜日行(
- 日付セル(.cell)
1日ごとの箱。クリックすると「イベント追加」モーダルを開きます。 - モーダル(#modal)
イベントの「タイトル/開始日/終了日/色(5色のボタン)/メモ」を入力し、「登録」「削除」「キャンセル」を操作。
役割がわかるIDやクラス名(weeks, cell, saveEventBtn など)を付けると、後の保守や拡張が楽になります。
CSS:見やすさと“重なり対策”
- 配色とベース
:rootに色やセル高(--cell-base)を定義し、全体のトーンを統一。 - 土日表示
.weekdays .sunと.weekdays .satで色を切り替え。セル側も.cell.sun / .cell.satで日付色を強調。 - イベント帯のレイヤ(.tracks)
各週に1枚「帯用のレイヤ」を重ね、そこに絶対配置でイベント帯(.bar)を並べます。 - 週の高さ“自動拡張”
イベント帯の段(レーン)数に応じて、週のセルにmin-heightを追加で上乗せ。帯が増えるほどセルがじわっと高くなるので、潰れません。
見やすさ(色・余白・高さ調整)は、使いやすさと直結します。CSSは“読みやすいカレンダー”の大黒柱です。
コード解説(JavaScript全体の解説)
データの準備と保存(LocalStorage)
- イベント配列:
EVENTS = []に予定を入れて管理します。 - 起動時の復元:
|
1 2 3 4 5 6 |
const KEY='uys-monthcal-v1'; let EVENTS = []; try{ const raw = localStorage.getItem(KEY); if(raw){ EVENTS = JSON.parse(raw)||[]; } // 起動時に復元 }catch{} |
ブラウザに保存済みなら読み戻し。なければ空配列。
- 明示保存/削除:
|
1 2 |
function saveToStorage(){ localStorage.setItem(KEY, JSON.stringify(EVENTS)); /* 端末保存 */ } function clearStorage(){ localStorage.removeItem(KEY); /* 保存データ削除 */ } |
自動保存ではなく、ボタンで明示的に保存。混乱を避けるため。
日付の計算と描画(render() が心臓部)
- 年月の見出し:
|
1 |
titleEl.textContent = current.getFullYear() + '年' + (current.getMonth()+1) + '月'; |
- 月の範囲と“表示範囲”の決定:
|
1 2 3 4 5 6 |
const y=current.getFullYear(), m=current.getMonth(); const first=new Date(y,m,1), last=new Date(y,m+1,0); // 1日が何曜日から始まるか、末日が何曜日で終わるか // 前月・翌月の補完日を含めて配列に並べる const start=new Date(first); start.setDate(first.getDate()-first.getDay()); const end=new Date(last); end.setDate(last.getDate()+(6-last.getDay())); |
カレンダーは日曜〜土曜で端が揃う長方形にしたいので、前後の月も淡色で埋めます。
- 日付を週ごとに敷き詰める:
|
1 2 3 4 5 6 7 |
const days=[]; const ptr=new Date(start); while(ptr<=end){ days.push(new Date(ptr)); ptr.setDate(ptr.getDate()+1); } for(let i=0;i<days.length;i+=7){ const wk=days.slice(i,i+7); // .week要素を作成し、7つの .cell(日付セル)を並べる } |
7日ずつ切り出し → .week(1週間)を積み上げます。
- セル生成(クリックで追加):
|
1 2 3 |
cell.className='cell'+(d.getMonth()!==m?' out':'')+(dow===0?' sun':'')+(dow===6?' sat':''); cell.innerHTML=`<div class="date">${d.getDate()}</div>`; cell.addEventListener('click',()=>openModal({start:fmt(d),end:fmt(d)})); |
当月外は淡色(out)、日曜赤(sun)、土曜青(sat)。
クリックで その日を初期値に してモーダルを開きます。
イベントの描画(“帯”を週レイヤに敷く)
- 表示中の週にかかるイベントだけ抽出:
|
1 2 3 |
const slice=EVENTS.map(e=>({...e,s:parse(e.start),e2:parse(e.end)})) .filter(e=> e.s<=wkEnd && e.e2>=wkStart) .sort((a,b)=> a.s-b.s || a.e2-b.e2); |
週の開始〜終了に少しでも重なるイベントを拾います。
- 列(1〜7)にマッピング & レーン(lane)で重なり回避:
|
1 2 3 |
const colStart=Math.max(1, daysBetween(wkStart,ev.s)+1); const colEnd=Math.min(7, daysBetween(wkStart,ev.e2)+1); // lane(段)を探して配置。衝突しない最上段に積む |
かぶるイベントは上下に段積みして見やすく。
- 週の高さを自動拡張:
|
1 2 |
const laneH=24; const extra = lanes.length ? (lanes.length*laneH+6) : 0; cellRefs.forEach(c=> c.style.minHeight=`calc(var(--cell-base) + ${extra}px)`); |
帯が増えるほどセルが高くなり、潰れません。
- 帯DOMを配置(%+pxでピタッとフィット):
|
1 2 3 |
div.style.left = `calc(${leftPct}% + 2px)`; div.style.right= `calc(${rightPct}% + 2px)`; div.style.top = (b.lane*laneH)+'px'; |
左右はパーセント、縦位置はpxで段ごとに。
イベントの追加・編集・削除(モーダル)
- 開く:
|
1 2 3 4 5 |
function openModal(ev){ editingId=ev.id||null; // 新規= null / 編集= 既存ID // フォームへ初期値セット(セルからなら当日で初期化) // 色ボタンの .active 切替、削除ボタンは編集時のみ表示 } |
PNG書き出し(html2canvas)
- ボタン類だけ一時非表示 → 年月は残す:
|
1 2 3 4 5 |
document.body.classList.add('exporting'); // .no-export をCSSで非表示 const canvas=await html2canvas(document.getElementById('captureArea'), { backgroundColor:'#ffffff', scale:2 }); document.body.classList.remove('exporting'); |
白背景&2倍スケールでくっきり出力。年月はPNGに含まれます。
- ダウンロード:
|
1 2 3 4 |
const a=document.createElement('a'); a.href=canvas.toDataURL('image/png'); a.download='calendar.png'; a.click(); |
色選択(5色をワンタップ)
|
1 2 3 4 5 6 7 |
let colorSelected='c1'; [...document.querySelectorAll('.color-btn')].forEach(btn=>{ btn.addEventListener('click', ()=>{ colorSelected=btn.dataset.color; // すべての .color-btn から .active を外し、選んだボタンにだけ付与 }); }); |
保存時に colorSelected をイベントへ保持 → 描画時に.bar cNで反映。
応用テクニック|カレンダーをもっと便利にするカスタマイズ例
ここからは、完成版コードをベースに 「こんな機能を足したらもっと便利!」 というアイデアをご紹介します。初心者の方はそのままでも十分使えますが、少し改造すると自分だけのオリジナルカレンダーになります。
繰り返し予定(定期イベント)
毎週・毎月繰り返すイベント(例:毎週月曜の会議、毎月1日の支払日)を登録できると便利です。
実装のヒント:
- イベントに
repeat: 'weekly'やrepeat: 'monthly'などの属性を追加 render()の中で、該当する日付をループして複製表示
祝日や記念日の自動表示
日本の祝日やユーザーが決めた記念日を自動で表示することも可能です。
実装のヒント:
- 祝日データをJSONで用意
render()実行時に、日付と突き合わせてセルにクラスを付与- 例えば「元日」を赤文字で固定表示
週始まりを変更(日曜始まり/月曜始まり)
日本のカレンダーは日曜始まりが一般的ですが、海外では月曜始まりが標準です。
実装のヒント:
first.getDay()の計算ロジックを切り替える- トグルボタンで「日曜始まり/月曜始まり」をユーザーが選択できるようにする
まずは基本機能に慣れてから、一つずつカスタマイズを試してみましょう。ほんの少しの改造でも“自分仕様”になると、愛着がわいて長く使えるツールになりますよ。
おすすめの学習リソース|HTMLやJavaScriptをさらに学びたい方へ
カレンダーツールを作る過程で「もっとJavaScriptを理解したい」「自分で応用できるようになりたい」と思った方へ、初心者にもわかりやすいおすすめリソースをご紹介します。これらを学ぶことで、今回のようなカレンダーに加えて、より複雑なWebアプリにも挑戦できるようになります。
おすすめの書籍
『確かな力が身につくJavaScript超入門』(SBクリエイティブ)
- JavaScriptを基礎から丁寧に学べる入門書。
- 実践的なサンプルも豊富で、今回のツールのような小さなアプリを自分で作れるようになります。
- 初学者にぴったりの1冊です。
『スラスラわかるHTML&CSSのきほん』(SBクリエイティブ)
- Web制作初心者に最適な入門書。HTMLとCSSの仕組みをやさしく解説。
- これからWebページを作ってみたい方にぴったり。
- レイアウトの基本やスタイルの調整方法など、実践的に学べます。
オンライン講座編
Udemy|世界最大級のオンライン学習プラットフォーム
世界中で利用されるオンライン学習サイト。
HTML、CSS、JavaScriptの入門から応用まで、高評価の講座が数百種類揃っています。
初心者でも動画を見ながら手を動かせるので、挫折しにくいのが魅力です。
本や講座で“基礎→応用”の流れをしっかり押さえると、自分でアレンジできる力がつきます。今回のカレンダーを題材に、学んだことを実際に手を動かして試してみるのが一番の近道ですよ。
まとめ|カレンダーツール作りから学べること
今回の記事では、「複数日イベントに対応したカレンダーWebツール」を、HTML・CSS・JavaScriptを組み合わせて実装する方法をご紹介しました。
学びのポイント
- 日付の計算やイベントの配置ロジックを理解することで、Webアプリの裏側の仕組みを実感できたはずです。
- UI(ユーザーインターフェース)の工夫や、保存・削除などの操作性をどうデザインするかは、今後どんなWebアプリを作るにも役立ちます。
- 完成したツールはそのまま実用できますが、コードを少しずつ改良して「自分仕様」にするのも大きな学びにつながります。
この記事を参考に、まずはシンプルなツール作りから始めてみましょう。
自分で作ったものが形になる体験は、必ず次のアイデアや学びにつながります。
ぜひ今回のカレンダーツールを、あなたのWebアプリ開発の第一歩として活用してみてください。
関連記事
【実際のカレンダー作成ツールの動作を試してみたい方はこちらからどうぞ】▼


