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);
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
No hour.
No minute.
No timezone.
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
}
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
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
};
}
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
The month is basically:
leading empty cells
+
actual dates
+
trailing empty cells
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;
});
}
Why 42?
7 columns × 6 rows = 42 cells
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
and this:
MON TUE WED THU FRI SAT SUN
should not require two calendar engines.
They are simply different configurations.
buildMonthGrid(2027, 1, 0);
// Sunday-first
buildMonthGrid(2027, 1, 1);
// Monday-first
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"
];
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
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;
}
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;
}
}
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;
}
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;
}
}
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
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>
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>
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
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();
}
Now:
daysInMonth(2027, 2);
// 28
and:
daysInMonth(2028, 2);
// 29
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
);
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
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)
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
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