Introduction
Building a custom calendar with event divs gives you full control over layout, styling, and interaction that pre-built libraries cannot match. The approach involves generating a month grid in HTML (using a table or CSS Grid), rendering event blocks as positioned div elements within day cells, and handling navigation and event CRUD with JavaScript. This article walks through building a functional monthly calendar from scratch with HTML, CSS, and vanilla JavaScript, including event display, multi-day events, and click interactions.
HTML Structure
1<div class="calendar-container">
2 <div class="calendar-header">
3 <button id="prev-month"><</button>
4 <h2 id="month-year">March 2026</h2>
5 <button id="next-month">></button>
6 </div>
7
8 <div class="calendar-grid">
9 <div class="day-header">Sun</div>
10 <div class="day-header">Mon</div>
11 <div class="day-header">Tue</div>
12 <div class="day-header">Wed</div>
13 <div class="day-header">Thu</div>
14 <div class="day-header">Fri</div>
15 <div class="day-header">Sat</div>
16 <!-- Day cells generated by JavaScript -->
17 </div>
18</div>
CSS Styling
1.calendar-container {
2 max-width: 900px;
3 margin: 0 auto;
4 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
5}
6
7.calendar-header {
8 display: flex;
9 justify-content: space-between;
10 align-items: center;
11 padding: 16px;
12}
13
14.calendar-header button {
15 background: none;
16 border: 1px solid #ddd;
17 border-radius: 4px;
18 padding: 8px 16px;
19 cursor: pointer;
20 font-size: 16px;
21}
22
23.calendar-grid {
24 display: grid;
25 grid-template-columns: repeat(7, 1fr);
26 border: 1px solid #e0e0e0;
27}
28
29.day-header {
30 padding: 8px;
31 text-align: center;
32 font-weight: 600;
33 background: #f5f5f5;
34 border-bottom: 1px solid #e0e0e0;
35}
36
37.day-cell {
38 min-height: 120px;
39 border: 1px solid #e0e0e0;
40 padding: 4px;
41 position: relative; /* For positioning event divs */
42}
43
44.day-cell.other-month {
45 background: #fafafa;
46 color: #ccc;
47}
48
49.day-cell.today {
50 background: #e3f2fd;
51}
52
53.day-number {
54 font-size: 14px;
55 font-weight: 500;
56 margin-bottom: 4px;
57}
58
59.event-div {
60 background: #1976d2;
61 color: white;
62 padding: 2px 6px;
63 margin: 2px 0;
64 border-radius: 3px;
65 font-size: 12px;
66 cursor: pointer;
67 overflow: hidden;
68 white-space: nowrap;
69 text-overflow: ellipsis;
70}
71
72.event-div:hover {
73 opacity: 0.85;
74}
75
76.event-div.multi-day {
77 border-radius: 0;
78 margin-right: -4px;
79 margin-left: -4px;
80}
81
82.event-div.multi-day.start {
83 border-radius: 3px 0 0 3px;
84 margin-left: 0;
85}
86
87.event-div.multi-day.end {
88 border-radius: 0 3px 3px 0;
89 margin-right: 0;
90}
91
92/* Event color categories */
93.event-div.work { background: #1976d2; }
94.event-div.personal { background: #388e3c; }
95.event-div.urgent { background: #d32f2f; }
96.event-div.meeting { background: #7b1fa2; }
JavaScript: Calendar Generation
1class Calendar {
2 constructor(container) {
3 this.container = container;
4 this.currentDate = new Date();
5 this.events = [];
6 this.init();
7 }
8
9 init() {
10 document.getElementById("prev-month").addEventListener("click", () => {
11 this.currentDate.setMonth(this.currentDate.getMonth() - 1);
12 this.render();
13 });
14
15 document.getElementById("next-month").addEventListener("click", () => {
16 this.currentDate.setMonth(this.currentDate.getMonth() + 1);
17 this.render();
18 });
19
20 this.render();
21 }
22
23 render() {
24 const year = this.currentDate.getFullYear();
25 const month = this.currentDate.getMonth();
26
27 // Update header
28 const monthNames = [
29 "January", "February", "March", "April", "May", "June",
30 "July", "August", "September", "October", "November", "December",
31 ];
32 document.getElementById("month-year").textContent =
33 `${monthNames[month]} ${year}`;
34
35 // Calculate grid
36 const firstDay = new Date(year, month, 1).getDay();
37 const daysInMonth = new Date(year, month + 1, 0).getDate();
38 const daysInPrevMonth = new Date(year, month, 0).getDate();
39
40 // Clear existing day cells
41 const grid = document.querySelector(".calendar-grid");
42 grid.querySelectorAll(".day-cell").forEach((el) => el.remove());
43
44 // Previous month's trailing days
45 for (let i = firstDay - 1; i >= 0; i--) {
46 const day = daysInPrevMonth - i;
47 this.createDayCell(grid, day, year, month - 1, true);
48 }
49
50 // Current month days
51 const today = new Date();
52 for (let day = 1; day <= daysInMonth; day++) {
53 const isToday =
54 day === today.getDate() &&
55 month === today.getMonth() &&
56 year === today.getFullYear();
57 this.createDayCell(grid, day, year, month, false, isToday);
58 }
59
60 // Next month's leading days
61 const totalCells = firstDay + daysInMonth;
62 const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
63 for (let day = 1; day <= remaining; day++) {
64 this.createDayCell(grid, day, year, month + 1, true);
65 }
66 }
67
68 createDayCell(grid, day, year, month, isOtherMonth, isToday = false) {
69 const cell = document.createElement("div");
70 cell.className = "day-cell";
71 if (isOtherMonth) cell.classList.add("other-month");
72 if (isToday) cell.classList.add("today");
73
74 const number = document.createElement("div");
75 number.className = "day-number";
76 number.textContent = day;
77 cell.appendChild(number);
78
79 // Add events for this day
80 const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
81 const dayEvents = this.getEventsForDate(dateStr);
82 dayEvents.forEach((event) => {
83 const eventDiv = document.createElement("div");
84 eventDiv.className = `event-div ${event.category || ""}`;
85 eventDiv.textContent = event.title;
86 eventDiv.addEventListener("click", (e) => {
87 e.stopPropagation();
88 this.onEventClick(event);
89 });
90 cell.appendChild(eventDiv);
91 });
92
93 // Click to add event
94 cell.addEventListener("click", () => {
95 if (!isOtherMonth) {
96 this.onDayClick(dateStr);
97 }
98 });
99
100 grid.appendChild(cell);
101 }
102
103 addEvent(event) {
104 this.events.push(event);
105 this.render();
106 }
107
108 getEventsForDate(dateStr) {
109 return this.events.filter((e) => e.date === dateStr);
110 }
111
112 onDayClick(dateStr) {
113 const title = prompt(`Add event for ${dateStr}:`);
114 if (title) {
115 this.addEvent({ title, date: dateStr, category: "work" });
116 }
117 }
118
119 onEventClick(event) {
120 alert(`Event: ${event.title}\nDate: ${event.date}`);
121 }
122}
123
124// Initialize
125const calendar = new Calendar(document.querySelector(".calendar-container"));
126
127// Add sample events
128calendar.addEvent({ title: "Team Meeting", date: "2026-03-05", category: "meeting" });
129calendar.addEvent({ title: "Deploy v2.0", date: "2026-03-12", category: "work" });
130calendar.addEvent({ title: "Doctor Appt", date: "2026-03-18", category: "personal" });
131calendar.addEvent({ title: "Deadline", date: "2026-03-25", category: "urgent" });
Multi-Day Events
1// Add to Calendar class
2renderMultiDayEvent(event) {
3 const start = new Date(event.startDate);
4 const end = new Date(event.endDate);
5 const cells = document.querySelectorAll('.day-cell');
6
7 cells.forEach(cell => {
8 const cellDate = cell.dataset.date;
9 if (!cellDate) return;
10
11 const current = new Date(cellDate);
12 if (current >= start && current <= end) {
13 const div = document.createElement('div');
14 div.className = `event-div multi-day ${event.category || ''}`;
15 div.textContent = current.getTime() === start.getTime()
16 ? event.title
17 : '';
18
19 if (current.getTime() === start.getTime()) div.classList.add('start');
20 if (current.getTime() === end.getTime()) div.classList.add('end');
21
22 cell.appendChild(div);
23 }
24 });
25}
Event Data Model
1// Example event structure
2const events = [
3 {
4 id: 1,
5 title: "Sprint Planning",
6 date: "2026-03-02",
7 startTime: "09:00",
8 endTime: "10:30",
9 category: "meeting",
10 color: "#7b1fa2",
11 },
12 {
13 id: 2,
14 title: "Conference",
15 startDate: "2026-03-15",
16 endDate: "2026-03-17",
17 category: "work",
18 allDay: true,
19 },
20];
Common Pitfalls
Off-by-one errors in month calculations: JavaScript months are 0-indexed (January = 0). new Date(2026, 2, 1) is March 1st, not February 1st. The daysInMonth trick new Date(year, month + 1, 0).getDate() uses day 0 to get the last day of the previous month.
Not handling timezone differences for date comparisons: new Date("2026-03-15") is parsed as UTC midnight, which may be a different local date depending on the user's timezone. Use new Date(year, month, day) for local dates, or normalize all dates to the same timezone.
Forgetting position: relative on day cells: Event divs positioned with position: absolute inside a day cell require the cell to have position: relative. Without it, events are positioned relative to the nearest positioned ancestor, breaking the layout.
Rendering too many events without overflow handling: If a day has more than 3-4 events, the cell overflows. Add a "+N more" indicator and a max-height with overflow hidden to keep the grid aligned across rows.
Rebuilding the entire DOM on every interaction: Calling render() after every event addition destroys and recreates all cells, which is slow with many events. Use targeted DOM updates (modify only affected cells) or a virtual DOM library for better performance.
Summary
Build the calendar grid using CSS Grid with 7 columns for days of the week
Generate day cells dynamically based on the current month, including trailing/leading days from adjacent months
Render events as positioned div elements inside day cells, styled with category-specific colors
Handle multi-day events by spanning event divs across consecutive cells with border-radius adjustments
Use position: relative on day cells and handle overflow with "+N more" indicators for busy days