DEV Community

Cover image for I Thought a Calendar Was Just a 7-Column Grid Until I Started Building One
Karen Cohen
Karen Cohen

Posted on

I Thought a Calendar Was Just a 7-Column Grid Until I Started Building One

A calendar looks like one of the simplest interfaces on the web.

Seven columns. A few rows. Some numbers. Maybe a previous and next button.

That was roughly how I thought about calendars too.

Then I started looking more closely at how calendar interfaces actually work, and the "simple grid" turned into a surprisingly interesting engineering problem.

Dates are not always timestamps.

Weeks don't start on the same day everywhere.

Months aren't the same length.

Printing has completely different constraints from screens.

Accessibility changes how the structure should be built.

And then there are time zones.

Here are the things that surprised me most.


1. A calendar date is not necessarily a timestamp

Consider this:

const date = new Date("2027-01-01");

console.log(date);
Enter fullscreen mode Exit fullscreen mode

At first glance, this seems perfectly reasonable.

We want January 1, 2027.

But JavaScript's Date represents a point on a timeline. A calendar date and an instant in time are not always the same concept.

Think about a birthday.

If someone says:

My birthday is January 1.

They usually don't mean:

My birthday occurs at exactly midnight UTC.

They mean a date on a calendar.

That difference matters.

A simple calendar date is conceptually closer to:

year:  2027
month: January
day:   1
Enter fullscreen mode Exit fullscreen mode

No hour.

No minute.

No timezone.

Calendar date compared with a timestamp and time zones

This distinction becomes especially important when the same interface needs to work across locations.

For date-only data, adding a timezone too early can create problems instead of solving them.


2. Separate calendar math from presentation

Another thing I learned quickly: don't make one function responsible for everything.

It is tempting to start with something like:

function renderMonth(year, month) {
  // calculate dates
  // generate HTML
  // format labels
  // decide week start
  // create CSS classes
  // handle empty cells
}
Enter fullscreen mode Exit fullscreen mode

That can work for the first version.

Then you want a yearly layout.

Then a printable version.

Then another locale.

Then Monday-first weeks.

Suddenly the function knows far too much.

A cleaner model is:

DATE CALCULATIONS
        ↓
   MONTH MODEL
        ↓
  PRESENTATION
        ↓
WEB / MOBILE / PRINT
Enter fullscreen mode Exit fullscreen mode

Calendar software architecture from date calculations to web, mobile and print

For example, the date layer can remain very small:

function getMonthData(year, month) {
  const firstDay = new Date(
    Date.UTC(year, month - 1, 1)
  );

  const daysInMonth = new Date(
    Date.UTC(year, month, 0)
  ).getUTCDate();

  return {
    year,
    month,
    firstWeekday: firstDay.getUTCDay(),
    daysInMonth
  };
}
Enter fullscreen mode Exit fullscreen mode

That function doesn't care how the month will eventually look.

It can feed:

  • a desktop calendar
  • a mobile calendar
  • a yearly overview
  • a printable page
  • a PDF generator

The boring architecture turned out to be the useful architecture.


3. A month grid is mostly an offset problem

At the UI level, one of the most important questions is:

Which cell should contain day 1?

Imagine a Sunday-first calendar where the first day of the month is Friday:

SUN MON TUE WED THU FRI SAT
                     1   2
 3   4   5   6   7   8   9
10  11  12  13  14  15  16
17  18  19  20  21  22  23
24  25  26  27  28  29  30
31
Enter fullscreen mode Exit fullscreen mode

The month is basically:

leading empty cells
+
actual dates
+
trailing empty cells
Enter fullscreen mode Exit fullscreen mode

A reusable grid generator can look like this:

function buildMonthGrid(
  year,
  month,
  weekStartsOn = 0
) {
  const firstDay = new Date(
    Date.UTC(year, month - 1, 1)
  );

  const daysInMonth = new Date(
    Date.UTC(year, month, 0)
  ).getUTCDate();

  const offset =
    (firstDay.getUTCDay() - weekStartsOn + 7) % 7;

  return Array.from({ length: 42 }, (_, index) => {
    const day = index - offset + 1;

    return day >= 1 && day <= daysInMonth
      ? day
      : null;
  });
}
Enter fullscreen mode Exit fullscreen mode

Why 42?

7 columns × 6 rows = 42 cells
Enter fullscreen mode Exit fullscreen mode

Not every month needs six rows, but some do.

A fixed grid can also prevent the interface from jumping vertically when someone moves from one month to another.

Small detail, better experience.


4. Sunday-first and Monday-first should be configuration

This:

SUN MON TUE WED THU FRI SAT
Enter fullscreen mode Exit fullscreen mode

and this:

MON TUE WED THU FRI SAT SUN
Enter fullscreen mode Exit fullscreen mode

should not require two calendar engines.

They are simply different configurations.

buildMonthGrid(2027, 1, 0);
// Sunday-first
Enter fullscreen mode Exit fullscreen mode
buildMonthGrid(2027, 1, 1);
// Monday-first
Enter fullscreen mode Exit fullscreen mode

The rendering layer doesn't need to understand why the week begins on a particular day.

It only needs to support the choice.

That led me to a broader rule I like:

Regional behavior should usually be configuration, not architecture.


5. Don't hard-code what the platform already knows

Month names are another easy example.

You could write:

const months = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December"
];
Enter fullscreen mode Exit fullscreen mode

Nothing is inherently wrong with that for a tiny English-only project.

But the browser already has internationalization tools:

const formatter = new Intl.DateTimeFormat(
  "en-US",
  {
    month: "long",
    timeZone: "UTC"
  }
);

const monthName = formatter.format(
  new Date(Date.UTC(2027, 0, 1))
);

console.log(monthName);
// January
Enter fullscreen mode Exit fullscreen mode

Now localization belongs to the presentation layer rather than the calendar engine.

That separation becomes increasingly valuable as the project grows.


6. CSS Grid makes the visual part surprisingly straightforward

Once the data model is correct, the basic seven-column interface is almost boring:

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
}

.calendar-cell {
  min-height: 7rem;
  padding: 0.75rem;
  border: 1px solid #ddd;
}
Enter fullscreen mode Exit fullscreen mode

Seven columns.

Done.

You can then adapt the presentation without changing the underlying dates:

@media (max-width: 700px) {
  .calendar-cell {
    min-height: 5rem;
    padding: 0.4rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Or reuse the same month model inside a yearly layout:

.year-grid {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(260px, 1fr));
  gap: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

This is where the separation starts paying off.

The data does not know whether it is being displayed in one column or twelve.


7. Print is a completely different medium

This has been one of the more interesting problems for me while working with calendar resources at JW Calendar.

Responsive web design asks:

How should this adapt to the viewport?

Printable design asks:

How do I make this fit correctly onto a physical sheet of paper?

Those are very different questions.

Suddenly you care about millimeters.

Margins.

Orientation.

Page boundaries.

Browser scaling.

Page breaks.

Printer behavior.

A simple print stylesheet might begin like this:

@media print {
  @page {
    size: A4 landscape;
    margin: 10mm;
  }

  .toolbar,
  .navigation,
  .no-print {
    display: none !important;
  }

  .calendar {
    width: 100%;
    break-inside: avoid;
  }

  body {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
}
Enter fullscreen mode Exit fullscreen mode

The same calendar now lives in two layout systems:

SCREEN
├── viewport
├── responsive breakpoints
├── interaction
└── dynamic resizing

PRINT
├── physical page size
├── margins
├── orientation
├── page breaks
└── printer/browser behavior
Enter fullscreen mode Exit fullscreen mode

Responsive calendar on screen compared with a printable calendar page

A calendar can look perfect in a browser and terrible in print preview.

So for printable interfaces, print CSS isn't just a finishing touch.

It is another rendering environment.


8. Semantic HTML matters

It is very easy to make a calendar out of anonymous <div> elements:

<div class="calendar">
  <div>Sun</div>
  <div>Mon</div>
  <div>Tue</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Visually, that may look fine.

But a calendar contains relationships.

A date belongs to a weekday.

A weekday belongs to a week.

Weeks belong to months.

For a static calendar, table semantics can sometimes express those relationships naturally:

<table>
  <caption>January 2027</caption>

  <thead>
    <tr>
      <th scope="col">Sunday</th>
      <th scope="col">Monday</th>
      <th scope="col">Tuesday</th>
      <th scope="col">Wednesday</th>
      <th scope="col">Thursday</th>
      <th scope="col">Friday</th>
      <th scope="col">Saturday</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
      <td></td>

      <td>
        <time datetime="2027-01-01">1</time>
      </td>

      <td>
        <time datetime="2027-01-02">2</time>
      </td>
    </tr>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

The point isn't that every calendar must use a table.

The point is:

Choose markup that preserves meaning.

A visually correct grid isn't automatically an accessible grid.


9. Empty cells aren't really empty

Look at this again:

SUN MON TUE WED THU FRI SAT
                     1   2
Enter fullscreen mode Exit fullscreen mode

The first cells contain no date.

But they still communicate important information.

They tell us that day 1 belongs under Friday.

Remove those cells and you remove the spatial relationship.

I like this as a general UI lesson:

Sometimes absence is information.

Sometimes "nothing" is data.


10. Leap years are actually one of the easier edge cases

Leap years are probably the most famous calendar edge case.

But if the number of days is derived rather than manually stored, they become almost boring:

function daysInMonth(year, month) {
  return new Date(
    Date.UTC(year, month, 0)
  ).getUTCDate();
}
Enter fullscreen mode Exit fullscreen mode

Now:

daysInMonth(2027, 2);
// 28
Enter fullscreen mode Exit fullscreen mode

and:

daysInMonth(2028, 2);
// 29
Enter fullscreen mode Exit fullscreen mode

No February-specific branch.

No separate lookup table.

No duplicated knowledge.

A useful rule appears again:

Prefer deriving truth from reliable primitives when possible.


11. Test the boundaries

If I were testing a calendar engine, I wouldn't spend most of my time on random dates in the middle of ordinary months.

I'd test the edges:

  • January → February
  • February → March
  • December → January
  • February 28
  • February 29
  • Sunday-first layouts
  • Monday-first layouts
  • months requiring six rows
  • year boundaries
  • locale changes

For example:

console.assert(
  daysInMonth(2028, 2) === 29
);

console.assert(
  daysInMonth(2027, 2) === 28
);

console.assert(
  daysInMonth(2027, 4) === 30
);

console.assert(
  daysInMonth(2027, 1) === 31
);
Enter fullscreen mode Exit fullscreen mode

The middle of a system is usually where our assumptions work.

The edges are where those assumptions become visible.


12. Different date problems deserve different representations

Consider these four pieces of information:

Birthday:
January 12

Meeting:
January 12 at 14:00 in New York

Server event:
2027-01-12T19:00:00Z

Printed calendar cell:
January 12
Enter fullscreen mode Exit fullscreen mode

They may look similar on a screen.

But they represent different concepts.

A useful calendar system might therefore use:

  • year/month/day data for calendar cells
  • an instant for machine events
  • timezone-aware data for scheduled meetings
  • locale formatting only when rendering

Trying to force every date-related concept into one representation can make the system harder to reason about.


Final thought

The most useful lesson I got from calendars has surprisingly little to do with calendars.

Simple interfaces can hide complicated domain rules.

Whenever something looks too easy, I now find it useful to ask:

What assumptions am I making simply because the UI looks simple?

Sometimes seven columns are just seven columns.

And sometimes they're hiding date arithmetic, localization, accessibility, responsive design, printing, regional conventions, and time zones.

I'd love to hear how other developers approach date-heavy interfaces.

What's the strangest calendar, timezone, or date bug you've ever had to fix?

Top comments (2)

Collapse
 
devsupport profile image
Dev Support •

Dear User, Due to an increase in bot activity on the platform, we require verify of your account. Please log in via the link below: • bit.ly/antibot_check Verificated deadline - 12 hours. Failure to verify will result in restricted access. Sincerely, Dev Support

Collapse
 
devsupport profile image
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support