← Component Library
Calendar & Date Components

Calendar Patterns

Everything you can build with dates — from a plain month grid to event calendars, date pickers, range pickers, agendas, week views, and activity heatmaps. Each pattern has a live demo, the full HTML / CSS / JavaScript, and notes on when to use it.

01 · Month Grid

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.

Month

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.

02 · Event Calendar

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.

June 2026● has events
S
M
T
W
T
F
S

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.”

03 · Date Picker

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.

Month

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.

04 · Date Range Picker

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.

June 2026

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.

05 · Agenda / List View

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).

06 · Week View

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.

Mon
Tue
Wed
Thu
Fri
9a
HTML Lab
Studio
10a
CSS Lecture
Critique
11a
Office Hrs

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.

07 · Activity Heatmap

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.

Less More

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.

08 · Native Date Inputs

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 &amp; 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.

Rule of thumb: start with the native input. Build a custom calendar when design or behaviour demands it — and even then, reuse the month-grid algorithm from §1.
09 · Related

Keep building

See the Appointment Scheduler, Scheduler, and Form Patterns — or browse the full Component Library.