Build Martin’s HTML Calendar: Step‑by‑Step TutorialMartin’s HTML Calendar is a simple, lightweight calendar you can add to any webpage using HTML, CSS, and a little JavaScript. This tutorial walks through building the calendar from scratch, explains the code, and shows ways to extend it with events, navigation, and responsive styling.
What you’ll build
By the end you’ll have a month-view calendar that:
- Displays the current month and year
- Shows day names and correctly aligned dates
- Allows month navigation (previous/next)
- Highlights today’s date
- Optionally can be extended to show events for specific dates
Project structure
Create a project folder and inside it add three files:
- index.html
- styles.css
- script.js
1) HTML structure (index.html)
Create a clean semantic structure for the calendar. Put the following in index.html:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Martin's HTML Calendar</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <main class="calendar-app"> <section class="calendar"> <header class="calendar-header"> <button id="prevMonth" aria-label="Previous month"><</button> <h2 id="monthYear">Month Year</h2> <button id="nextMonth" aria-label="Next month">></button> </header> <table class="calendar-grid" role="grid" aria-labelledby="monthYear"> <thead> <tr id="weekdayRow"></tr> </thead> <tbody id="calendarBody"></tbody> </table> </section> </main> <script src="script.js"></script> </body> </html>
2) Styling (styles.css)
Add styling that keeps the calendar simple, accessible, and responsive. Put this in styles.css:
:root{ --bg:#ffffff; --accent:#2b79ff; --muted:#f1f3f8; --text:#222; --today:#ffe082; --cell-size:64px; font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; } *{box-sizing:border-box} body{ margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; background:linear-gradient(180deg,#f7f9fc,#eef3ff); color:var(--text); padding:24px; } .calendar-app{width:100%;max-width:560px} .calendar{ background:var(--bg); border-radius:12px; box-shadow:0 6px 20px rgba(18,38,63,0.08); overflow:hidden; } .calendar-header{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:16px; border-bottom:1px solid #eef2fb; } .calendar-header h2{ margin:0; font-size:18px; font-weight:600; text-align:center; flex:1; } .calendar-header button{ background:transparent; border:1px solid transparent; font-size:18px; padding:8px 10px; border-radius:8px; cursor:pointer; } .calendar-grid{ width:100%; border-collapse:collapse; table-layout:fixed; } .calendar-grid thead th{ padding:10px 6px; font-weight:600; color:#5b6b88; font-size:13px; text-align:center; background:var(--muted); } .calendar-grid td{ height:var(--cell-size); vertical-align:top; padding:8px; border:1px solid #f1f4fb; text-align:left; } .day{ display:flex; gap:8px; align-items:flex-start; justify-content:space-between; } .date-number{ font-weight:600; color:var(--text); } .other-month{ color:#a6b0c3; opacity:0.9; } .today{ background:linear-gradient(90deg,var(--today),#fff); border-radius:8px; padding:6px; display:inline-block; } @media (max-width:420px){ :root{--cell-size:56px} .calendar-header h2{font-size:16px} }
3) JavaScript (script.js)
This script populates the calendar grid, handles month navigation, and highlights today.
const monthYear = document.getElementById('monthYear'); const weekdayRow = document.getElementById('weekdayRow'); const calendarBody = document.getElementById('calendarBody'); const prevBtn = document.getElementById('prevMonth'); const nextBtn = document.getElementById('nextMonth'); const weekdays = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; // change to Mon-first if desired const today = new Date(); let current = new Date(today.getFullYear(), today.getMonth(), 1); // render weekday headers function renderWeekdays(){ weekdayRow.innerHTML = ''; for (let d of weekdays){ const th = document.createElement('th'); th.scope = 'col'; th.textContent = d; weekdayRow.appendChild(th); } } // render the days for current month function renderCalendar(){ calendarBody.innerHTML = ''; const year = current.getFullYear(); const month = current.getMonth(); monthYear.textContent = current.toLocaleString(undefined,{month:'long', year:'numeric'}); const firstDayIndex = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const prevMonthDays = new Date(year, month, 0).getDate(); let row = document.createElement('tr'); // total cells = 6 weeks * 7 = 42 to keep layout stable for (let i=0; i<42; i++){ if (i % 7 === 0 && i !== 0){ calendarBody.appendChild(row); row = document.createElement('tr'); } const cell = document.createElement('td'); const cellContent = document.createElement('div'); cellContent.className = 'day'; let cellNumber = document.createElement('span'); cellNumber.className = 'date-number'; const dayIndex = i - firstDayIndex + 1; if (i < firstDayIndex){ // previous month's tail const d = prevMonthDays - (firstDayIndex - 1 - i); cellNumber.textContent = d; cellNumber.classList.add('other-month'); } else if (dayIndex > daysInMonth){ // next month head const d = dayIndex - daysInMonth; cellNumber.textContent = d; cellNumber.classList.add('other-month'); } else { // current month cellNumber.textContent = dayIndex; // highlight today if (year === today.getFullYear() && month === today.getMonth() && dayIndex === today.getDate()){ const highlight = document.createElement('span'); highlight.className = 'today'; highlight.textContent = dayIndex; cellContent.appendChild(highlight); } else { cellContent.appendChild(cellNumber); } } // ensure there's at least the number in place (for other-month cases) if (!cellContent.hasChildNodes()){ cellContent.appendChild(cellNumber); } cell.appendChild(cellContent); row.appendChild(cell); } // append last row calendarBody.appendChild(row); } // navigation prevBtn.addEventListener('click', ()=>{ current.setMonth(current.getMonth() - 1); renderCalendar(); }); nextBtn.addEventListener('click', ()=>{ current.setMonth(current.getMonth() + 1); renderCalendar(); }); // init renderWeekdays(); renderCalendar();
4) Accessibility and small improvements
- Add aria-live on the month/year element if you want screen readers notified when month changes.
- Make weekday names localizable (use toLocaleDateString with weekday option).
- Add keyboard support: left/right arrow to change months, Home/End to jump to current month.
- Use button elements for date cells if you plan to open event details or select dates.
5) Adding events (basic approach)
Store events as an object keyed by ISO date (YYYY-MM-DD). Example snippet to attach in script.js:
const events = { '2025-09-05': [{title:'Team meeting', time:'10:00'}], '2025-09-12': [{title:'Project deadline'}] }; function isoDate(year, month, day){ return `${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`; } // inside renderCalendar(), when creating a current-month cell: const id = isoDate(year, month, dayIndex); if (events[id]){ const dot = document.createElement('span'); dot.textContent = '•'; dot.title = events[id].map(e=>e.title).join(', '); dot.style.marginLeft = '6px'; cellContent.appendChild(dot); }
6) Responsive & advanced features
- Make the grid wrap on very small screens or change to a week-scroller view.
- Add support for selecting ranges, recurring events, drag-and-drop (requires more JS).
- Persist events using localStorage or sync with a backend via API.
Troubleshooting tips
- If dates are misaligned, check whether getDay() uses Sunday-first in your locale; adjust weekdays array or use options to force Monday-first.
- Always normalize times when comparing dates — use year/month/day integers rather than Date objects with time.
- For styling issues, inspect element heights; table-layout:fixed helps consistent cell sizes.
This gives you a full, working Martin’s HTML Calendar you can expand. If you want, I can:
- Add event popups with modal markup and code.
- Convert weekdays to Monday-first.
- Provide a version using modern frameworks (React/Vue/Svelte).
Leave a Reply