The foundation: a real month, generated with JavaScript
Every calendar starts here — a seven-column grid with one cell per day. Rather than hand-typing dates, let JavaScript figure out which weekday the month starts on and how many days it has. Use the arrows to move between months; today is highlighted.
HTML
<div class="bar"><button id="prev">‹</button><b id="label"></b><button id="next">›</button></div> <div class="grid" id="grid"></div>
JavaScript — the core algorithm
// Grab the elements from the HTML above
const grid = document.getElementById('grid');
const label = document.getElementById('label');
const MONTHS = ['January','February','March','April','May','June','July',
'August','September','October','November','December'];
function render(year, month) { // month is 0–11
const firstDay = new Date(year, month, 1).getDay(); // 0 = Sunday
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
let html = ['S','M','T','W','T','F','S'].map(d => `<div class="dow">${d}</div>`).join('');
for (let i = 0; i < firstDay; i++) html += '<div class="day empty"></div>'; // blanks
for (let d = 1; d <= daysInMonth; d++) {
const isToday = year === today.getFullYear() && month === today.getMonth() && d === today.getDate();
html += `<div class="day ${isToday ? 'today' : ''}">${d}</div>`;
}
grid.innerHTML = html;
label.textContent = MONTHS[month] + ' ' + year;
}
// Start on the current month, then wire the prev/next arrows
let y = new Date().getFullYear(), m = new Date().getMonth();
document.getElementById('prev').addEventListener('click', () => { if (--m < 0) { m = 11; y--; } render(y, m); });
document.getElementById('next').addEventListener('click', () => { if (++m > 11) { m = 0; y++; } render(y, m); });
render(y, m);
CSS
.bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.bar b { color: #111; font-size: 1rem; }
.bar button { width: 32px; height: 32px; border: 1px solid #cbd5e1; background: #fff;
border-radius: 8px; cursor: pointer; color: #374151; font-size: 1rem;
display: grid; place-items: center; }
.bar button:hover { border-color: #6366f1; color: #6366f1; }
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dow { text-align: center; font-size: .66rem; font-weight: 700; color: #9ca3af; padding: 4px 0; text-transform: uppercase; }
.day { aspect-ratio: 1; display: grid; place-items: center; border-radius: 8px;
font-size: .82rem; color: #374151; background: #fff; border: 1px solid #eef2f6; }
.day.empty { background: transparent; border: 0; }
.day.today { background: #6366f1; color: #fff; font-weight: 800; }
When to use: any time you need a familiar month view. It's the base layer — the event calendar, date picker, and range picker below are all this same grid with extra behaviour.
Mark the days that have something on them
Add a dot (or a coloured pill) to days with events, and show details when one is clicked. Click a day with a dot.
Click a marked day to see its event.
HTML
<div class="cal">
<div class="bar"><b>June 2026</b><span>● has events</span></div>
<div class="grid" id="grid">
<div class="dow">S</div><div class="dow">M</div><div class="dow">T</div><div class="dow">W</div><div class="dow">T</div><div class="dow">F</div><div class="dow">S</div>
<!-- day cells injected by JS; days with an event get class="day event" -->
</div>
<p class="pick-out" id="out">Click a marked day to see its event.</p>
</div>
JavaScript — store events in an object keyed by day
const grid = document.getElementById('grid');
const out = document.getElementById('out');
// which days have something on them
const events = { 3: 'Project kickoff', 9: 'Critique session', 11: 'Guest lecture', 18: 'Milestone due' };
// June 2026 starts on a Monday and has 30 days
const first = new Date(2026, 5, 1).getDay(); // 5 = June (months are 0-based)
let html = '';
for (let i = 0; i < first; i++) html += '<div class="day empty"></div>'; // blanks
for (let d = 1; d <= 30; d++) {
const cls = 'day' + (events[d] ? ' event pick' : ''); // dot + pointer if there's an event
html += `<div class="${cls}" data-day="${d}">${d}</div>`;
}
grid.insertAdjacentHTML('beforeend', html); // keep the S M T W T F S headers already in the grid
grid.addEventListener('click', e => {
const cell = e.target.closest('.day');
if (!cell) return;
const d = +cell.dataset.day;
if (events[d]) out.textContent = `June ${d}: ${events[d]}`;
});
CSS — the grid plus the event dot
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dow { text-align: center; font-size: .66rem; font-weight: 700; color: #9ca3af; padding: 4px 0; text-transform: uppercase; }
.day { aspect-ratio: 1; display: grid; place-items: center; border-radius: 8px;
font-size: .82rem; color: #374151; background: #fff; border: 1px solid #eef2f6;
position: relative; } /* relative so the dot can sit inside */
.day.empty { background: transparent; border: 0; }
.day.pick { cursor: pointer; }
.day.pick:hover { background: #eef2ff; }
.day.event::after { /* the little dot */
content: ""; position: absolute; bottom: 5px;
width: 5px; height: 5px; border-radius: 50%; background: #f59e0b;
}
When to use: class schedules, content calendars, booking overviews — anywhere people scan a month for “what's happening when.”
Let someone choose a single date
The month grid becomes a picker by making each day clickable and remembering the selection. Click any day to select it.
No date selected.
HTML
<div class="cal"> <div class="bar"><button id="prev">‹</button><b id="label"></b><button id="next">›</button></div> <div class="grid" id="grid"></div> <p class="pick-out" id="out">No date selected.</p> </div>
JavaScript
const grid = document.getElementById('grid');
const label = document.getElementById('label');
const out = document.getElementById('out');
const MONTHS = ['January','February','March','April','May','June','July',
'August','September','October','November','December'];
const pad = n => (n < 10 ? '0' : '') + n;
// Same month-grid algorithm as §1, but each day gets a data-date and the "pick" class
function render(year, month) {
const first = new Date(year, month, 1).getDay();
const days = new Date(year, month + 1, 0).getDate();
let html = ['S','M','T','W','T','F','S'].map(d => `<div class="dow">${d}</div>`).join('');
for (let i = 0; i < first; i++) html += '<div class="day empty"></div>';
for (let d = 1; d <= days; d++) {
const iso = `${year}-${pad(month + 1)}-${pad(d)}`; // e.g. 2026-06-12
html += `<div class="day pick" data-date="${iso}">${d}</div>`;
}
grid.innerHTML = html;
label.textContent = MONTHS[month] + ' ' + year;
}
let y = new Date().getFullYear(), m = new Date().getMonth();
document.getElementById('prev').addEventListener('click', () => { if (--m < 0) { m = 11; y--; } render(y, m); });
document.getElementById('next').addEventListener('click', () => { if (++m > 11) { m = 0; y++; } render(y, m); });
// Remember the clicked day
grid.addEventListener('click', e => {
const cell = e.target.closest('.day');
if (!cell || cell.classList.contains('empty')) return;
grid.querySelectorAll('.day').forEach(c => c.classList.remove('selected'));
cell.classList.add('selected');
out.textContent = 'Selected: ' + cell.dataset.date; // e.g. 2026-06-12
});
render(y, m);
CSS
.bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.bar button { width: 32px; height: 32px; border: 1px solid #cbd5e1; background: #fff;
border-radius: 8px; cursor: pointer; display: grid; place-items: center; }
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dow { text-align: center; font-size: .66rem; font-weight: 700; color: #9ca3af; padding: 4px 0; text-transform: uppercase; }
.day { aspect-ratio: 1; display: grid; place-items: center; border-radius: 8px;
font-size: .82rem; color: #374151; background: #fff; border: 1px solid #eef2f6; }
.day.empty { background: transparent; border: 0; }
.day.pick { cursor: pointer; }
.day.pick:hover { background: #eef2ff; }
.day.selected { background: #6366f1; color: #fff; } /* the chosen day */
.pick-out { margin-top: 10px; font-size: .85rem; color: #4338ca; font-weight: 700; }
When to use: forms that need a date — a birthday, a deadline. Tip: for most forms the native <input type="date"> (§8) is faster to build and accessible out of the box; build a custom picker only when you need custom styling or rules.
Pick a start and end, highlight the days between
For bookings and reports you often need a range. Click once for the start, once more for the end — the days in between fill in. Click two days.
Click a start date.
HTML
<div class="cal"> <div class="bar"><b>June 2026</b><button id="reset">Reset</button></div> <div class="grid" id="grid"></div> <p class="pick-out" id="out">Click a start date.</p> </div>
JavaScript — two clicks, then fill the middle
const grid = document.getElementById('grid');
const out = document.getElementById('out');
const reset = document.getElementById('reset');
// Build June 2026 (starts Monday, 30 days), each day clickable
const first = new Date(2026, 5, 1).getDay();
let html = ['S','M','T','W','T','F','S'].map(d => `<div class="dow">${d}</div>`).join('');
for (let i = 0; i < first; i++) html += '<div class="day empty"></div>';
for (let d = 1; d <= 30; d++) html += `<div class="day pick" data-day="${d}">${d}</div>`;
grid.innerHTML = html;
const cells = [...grid.querySelectorAll('.day:not(.empty)')];
let start = null, end = null;
function paint() {
cells.forEach(c => {
const d = +c.dataset.day;
c.classList.toggle('selected', d === start || d === end);
c.classList.toggle('inrange', !!(start && end && d > start && d < end));
});
if (start && !end) out.textContent = `Start: June ${start} — pick an end date.`;
else if (start && end) out.textContent = `June ${start} – June ${end} (${end - start + 1} days)`;
else out.textContent = 'Click a start date.';
}
grid.addEventListener('click', e => {
const cell = e.target.closest('.day');
if (!cell || cell.classList.contains('empty')) return;
const day = +cell.dataset.day;
if (start === null || end !== null) { start = day; end = null; } // 1st click
else { end = day; if (end < start) [start, end] = [end, start]; } // 2nd click
paint();
});
reset.addEventListener('click', () => { start = end = null; paint(); });
paint();
CSS
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dow { text-align: center; font-size: .66rem; font-weight: 700; color: #9ca3af; padding: 4px 0; text-transform: uppercase; }
.day { aspect-ratio: 1; display: grid; place-items: center; border-radius: 8px;
font-size: .82rem; color: #374151; background: #fff; border: 1px solid #eef2f6; }
.day.empty { background: transparent; border: 0; }
.day.pick { cursor: pointer; }
.day.selected { background: #6366f1; color: #fff; } /* the two ends */
.day.inrange { background: #e0e7ff; color: #3730a3; border-radius: 0; } /* days between */
.pick-out { margin-top: 10px; font-size: .85rem; color: #4338ca; font-weight: 700; }
When to use: hotel/flight bookings, “from–to” report filters, vacation requests — any time the answer is a span of days, not one.
Upcoming events as a simple list
Not every calendar needs a grid. An agenda lists events in date order — easier to read on phones and better for screen readers. It's just a styled list, no date math required.
HTML — one container; JS renders the items into it
<div class="agenda" id="agenda"></div> <!-- each item rendered by JS looks like this: --> <div class="ag-item"> <div class="ag-when"><div class="d">3</div><div class="m">Jun</div></div> <div class="ag-body"><div class="t">Project kickoff</div><div class="s">10:00 AM · Studio A</div></div> </div>
CSS
.agenda { display: grid; gap: 8px; }
.ag-item { display: flex; gap: 14px; background: #fff; border: 1px solid #e5e7eb;
border-radius: 10px; padding: 12px 14px; }
.ag-when .d { font-size: 1.3rem; font-weight: 800; color: #6366f1; line-height: 1; }
.ag-when .m { font-size: .66rem; text-transform: uppercase; color: #9ca3af; font-weight: 700; }
.ag-body .t { font-weight: 700; color: #111; font-size: .9rem; }
.ag-body .s { font-size: .78rem; color: #6b7280; }
JavaScript — render the list from a data array
const agenda = document.getElementById('agenda');
const events = [
{ d: 3, m: 'Jun', title: 'Project kickoff', sub: '10:00 AM · Studio A' },
{ d: 9, m: 'Jun', title: 'Critique session', sub: '1:00 PM · Room 204' },
{ d: 18, m: 'Jun', title: 'Milestone due', sub: 'All day' },
];
agenda.innerHTML = events.map(e => `
<div class="ag-item">
<div class="ag-when"><div class="d">${e.d}</div><div class="m">${e.m}</div></div>
<div class="ag-body"><div class="t">${e.title}</div><div class="s">${e.sub}</div></div>
</div>`).join('');
When to use: mobile layouts, “next 5 events” widgets, or as the small-screen fallback for a grid calendar (show the grid on desktop, the agenda on phones via a media query).
A timetable for a single week
When the time of day matters — classes, shifts, appointments — a week view puts hours down the side and days across the top, with events placed in the right slot. This is a CSS grid with a time column plus five day columns.
Click an event to select it.
HTML — header row, then one time label + 5 columns per hour
<div class="week"> <div class="h"></div><div class="h">Mon</div><div class="h">Tue</div><div class="h">Wed</div><div class="h">Thu</div><div class="h">Fri</div> <div class="time">9a</div><div class="ev">HTML Lab</div><div class="slot"></div><div class="ev green">Studio</div><div class="slot"></div><div class="slot"></div> <div class="time">10a</div><div class="slot"></div><div class="ev">CSS Lecture</div><div class="slot"></div><div class="slot"></div><div class="ev">Critique</div> <div class="time">11a</div><div class="slot"></div><div class="slot"></div><div class="slot"></div><div class="ev green">Office Hrs</div><div class="slot"></div> </div> <p class="pick-out" id="out">Click an event to select it.</p>
CSS
.week { display: grid; grid-template-columns: 48px repeat(5, 1fr); gap: 4px; font-size: .72rem; }
/* row 1 = day headers, then one row per time slot.
An event is just a coloured cell sitting in its day column + time row. */
.week .h { text-align: center; font-weight: 700; color: #6b7280; padding: 4px 0; }
.week .time { color: #9ca3af; text-align: right; padding-right: 6px; }
.week .slot { background: #fff; border: 1px solid #eef2f6; border-radius: 6px; min-height: 30px; }
.week .ev { background: #6366f1; color: #fff; border-radius: 6px; padding: 4px 6px;
font-weight: 700; font-size: .68rem; cursor: pointer; }
.week .ev.green { background: #16a34a; }
.week .ev.sel { outline: 2px solid #1e1b4b; outline-offset: 1px; } /* the chosen event */
.pick-out { margin-top: 10px; font-size: .85rem; color: #4338ca; font-weight: 700; }
JavaScript — click an event to select it
const week = document.querySelector('.week');
const out = document.getElementById('out');
week.addEventListener('click', e => {
const ev = e.target.closest('.ev'); if (!ev) return;
week.querySelectorAll('.ev').forEach(x => x.classList.remove('sel'));
ev.classList.add('sel'); // highlight the chosen event
out.textContent = 'Selected: ' + ev.textContent;
});
When to use: scheduling where the hour matters. For overlapping events or drag-to-create, reach for a library (FullCalendar) — but a static timetable like this is pure CSS.
A year of activity at a glance
The GitHub “contribution graph” pattern: one small square per day, coloured by how much happened. Squares flow top-to-bottom (one week per column). Great for showing streaks and habits over time.
HTML
<div class="heat" id="heat"><!-- one <span> per day, injected by JS --></div> <div class="heat-legend">Less <span></span><span class="l1"></span><span class="l2"></span><span class="l3"></span><span class="l4"></span> More</div>
CSS — columns of 7
.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 13px); gap: 3px; }
.heat span { width: 13px; height: 13px; border-radius: 3px; background: #ebedf0; }
.heat span.l1 { background: #c6e48b; } /* light */
.heat span.l2 { background: #7bc96f; }
.heat span.l3 { background: #44a340; }
.heat span.l4 { background: #1e6823; } /* darkest */
JavaScript — one square per day, level by value
const heat = document.getElementById('heat');
for (let i = 0; i < 119; i++) { // ~17 weeks
const level = Math.floor(Math.random() * 5); // 0–4 (real data goes here)
const cell = document.createElement('span');
if (level) cell.className = 'l' + level;
heat.appendChild(cell);
}
When to use: habit trackers, streaks, “days active,” commit/post history — dense overviews where each day is a tiny cell.
The easiest calendar: let the browser do it
Before building anything custom, remember the browser ships with date controls. They're accessible, localized, and open a real calendar popup for free. Click into the fields.
Pick a value above to see it here.
HTML — that's the whole thing
<div class="native"> <div><label for="nd">Pick a date</label><input id="nd" type="date" value="2026-06-12"></div> <div><label for="nm">Pick a month</label><input id="nm" type="month" value="2026-06"></div> <div><label for="nw">Pick a week</label><input id="nw" type="week"></div> <div><label for="ndt">Date & time</label><input id="ndt" type="datetime-local"></div> </div> <p class="pick-out" id="out">Pick a value above to see it here.</p> <!-- limit the range with min / max --> <!-- <input type="date" min="2026-01-01" max="2026-12-31"> -->
CSS
.native { display: grid; gap: 12px; max-width: 320px; }
.native label { font-size: .82rem; font-weight: 600; color: #374151; display: block; margin-bottom: 4px; }
.native input { width: 100%; padding: 9px 11px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: .9rem; color: #111; }
.native input:focus-visible { outline: 2px solid #6366f1; border-color: #6366f1; }
.pick-out { margin-top: 10px; font-size: .85rem; color: #4338ca; font-weight: 700; }
JavaScript — read the value when it changes
const out = document.getElementById('out');
document.querySelectorAll('.native input').forEach(input => {
input.addEventListener('change', () => {
out.textContent = input.value
? `${input.type} = ${input.value}` // e.g. date = 2026-06-12
: 'No value';
});
});
When to use: almost every form. Reach for a custom calendar (the patterns above) only when you need custom styling, multi-date selection, or rules the native control can't express.
Keep building
See the Appointment Scheduler, Scheduler, and Form Patterns — or browse the full Component Library.