DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

A Notion-Style Calendar Is Just One CSS Grid

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.

Top comments (0)