Notion’s calendar looks like a polished, complicated component. Build one and you’ll find it’s a 7-column CSS grid plus about three lines of date arithmetic. No date library, no calendar package.
This is Day 5 of my DesignFromZero series — recreate a slick UI from scratch and learn why it works.
Seven columns. That’s the layout.
.cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
Enter fullscreen mode Exit fullscreen mode
Drop your day cells in and the browser wraps them into weeks automatically. You never position a single cell by hand.
Where does the month start?
The 1st isn’t always a Sunday. Ask JavaScript which weekday it is:
const startDay = new Date(year, month, 1).getDay(); // 0=Sun … 6=Sat
Enter fullscreen mode Exit fullscreen mode
That number is exactly how many blank cells you prepend so dates line up under the right weekday header.
How many days this month?
The classic JS trick — day 0 of next month rolls back to the last day of this month:
const daysInMonth = new Date(year, month + 1, 0).getDate(); // 28/29/30/31
Enter fullscreen mode Exit fullscreen mode
Leap years handled for free.
Pad both ends so rows never look ragged
This is the detail that makes it read as “Notion” and not a school worksheet:
// leading: the previous month's last few days, dimmed grey
for (let i = startDay - 1; i >= 0; i--) addCell(prevMonthDays - i, /* dim */ true);
// this month
for (let d = 1; d <= daysInMonth; d++) addCell(d, false, new Date(year, month, d));
// trailing: next month's first days, so the final row is full
const trailing = (7 - (startDay + daysInMonth) % 7) % 7;
Enter fullscreen mode Exit fullscreen mode
Those grey overflow days are 90% of the “this looks professional” effect.
Today + events
Highlight today by comparing each cell’s ISO date string to new Date(). Events live in a plain object keyed by date — render whatever sits under that key as colored chips:
const events = { "2026-06-21": [{ t: "Ship v2 🚀" }] };
const key = iso(date); // "2026-06-21"
(events[key] || []).forEach(e => {
const chip = document.createElement("div");
chip.textContent = e.t;
cell.appendChild(chip);
});
Enter fullscreen mode Exit fullscreen mode
That’s Notion’s “database on a calendar” in miniature.
Month navigation
prevBtn.onclick = () => { view.setMonth(view.getMonth() - 1); render(); };
Enter fullscreen mode Exit fullscreen mode
Re-render and you’re done. The built-in Date object does all the arithmetic — no moment, no date-fns for a clean month view.
🗓️ See it live + click any day to add an event: https://dev48v.infy.uk/design/day5-notion-calendar.html
Day 5 of DesignFromZero. A new UI clone every day, built from scratch.
답글 남기기