Calendar Month View
Machine-coding brief for a month calendar grid — date math, selection, keyboard nav, a11y, and localization hooks.
- machine-coding
- interview
- react
- dates
Problem statement
Build a month calendar view: a 7-column grid for one month, with prev/next navigation and day selection. Interviewers care about date math edge cases (month boundaries, week start day), controlled selection, and keyboard accessibility — not a full scheduling product.
Requirements
Must have
- Show one month at a time (header: Month YYYY)
- 7-day week headers (configurable week start: Sunday or Monday)
- Leading/trailing days from adjacent months (dimmed) or empty cells — pick one and stick to it
- Click a day → select it (controlled or uncontrolled)
- Prev / next month controls
- Today indicator
- Keyboard: arrows move focus day-to-day; Enter selects; PageUp/PageDown change month
Should have
minDate/maxDatedisable out-of-range daysdisabledDatesset or predicate- Locale-aware month/weekday labels (
Intl.DateTimeFormat)
Nice to have
- Multi-select or range select
- Show dots for “has events” without building full event UI
- Year picker
Planning (5 minutes out loud)
- Canonical date — store as
YYYY-MM-DDstring or UTC-noonDateto avoid TZ bugs - Grid builder — pure function
(year, month, weekStartsOn) => Cell[] - Focus vs selection — two concepts (roving tabindex)
- MVP — grid + select + prev/next; then keyboard + min/max
- No date library unless allowed — show you can do month math
Architecture
Calendar
├── CalendarHeader // title + prev/next
├── WeekdayRow
├── CalendarGrid
│ └── DayCell
└── utils/calendarMath.ts
Data model
type DayCell = {
date: string; // YYYY-MM-DD
day: number;
inCurrentMonth: boolean;
isToday: boolean;
};
type CalendarProps = {
value?: string | null; // selected YYYY-MM-DD
defaultValue?: string | null;
onChange?: (date: string | null) => void;
month?: { year: number; month: number }; // 0-indexed month, controlled view
defaultMonth?: { year: number; month: number };
onMonthChange?: (m: { year: number; month: number }) => void;
weekStartsOn?: 0 | 1; // 0 = Sunday
minDate?: string;
maxDate?: string;
isDateDisabled?: (date: string) => boolean;
};
Date math sketch
function pad(n: number) {
return String(n).padStart(2, "0");
}
export function toKey(y: number, m: number, d: number) {
return `${y}-${pad(m + 1)}-${pad(d)}`;
}
export function parseKey(key: string) {
const [y, m, d] = key.split("-").map(Number);
return { year: y, month: m - 1, day: d };
}
export function daysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate();
}
export function buildMonthGrid(
year: number,
month: number,
weekStartsOn: 0 | 1 = 0
): DayCell[] {
const firstDow = new Date(year, month, 1).getDay(); // 0 Sun
const startOffset = (firstDow - weekStartsOn + 7) % 7;
const dim = daysInMonth(year, month);
const today = new Date();
const todayKey = toKey(today.getFullYear(), today.getMonth(), today.getDate());
const cells: DayCell[] = [];
// leading
const prevDim = daysInMonth(year, month - 1);
for (let i = startOffset - 1; i >= 0; i--) {
const d = prevDim - i;
const dt = new Date(year, month - 1, d);
cells.push({
date: toKey(dt.getFullYear(), dt.getMonth(), d),
day: d,
inCurrentMonth: false,
isToday: false,
});
}
for (let d = 1; d <= dim; d++) {
const key = toKey(year, month, d);
cells.push({
date: key,
day: d,
inCurrentMonth: true,
isToday: key === todayKey,
});
}
// trailing to complete weeks (42 cells = 6 weeks is fine)
let next = 1;
while (cells.length % 7 !== 0 || cells.length < 42) {
const dt = new Date(year, month + 1, next);
cells.push({
date: toKey(dt.getFullYear(), dt.getMonth(), next),
day: next,
inCurrentMonth: false,
isToday: false,
});
next++;
if (cells.length >= 42) break;
}
return cells;
}
Watch DST and “local midnight” pitfalls: constructing new Date("2026-08-04") parses as UTC in some engines. Prefer new Date(y, m, d) for local calendar math.
Implementation sketch
function DayButton({
cell,
selected,
disabled,
tabIndex,
onSelect,
onFocus,
}: {
cell: DayCell;
selected: boolean;
disabled: boolean;
tabIndex: number;
onSelect: () => void;
onFocus: () => void;
}) {
return (
<button
type="button"
role="gridcell"
aria-selected={selected}
aria-current={cell.isToday ? "date" : undefined}
disabled={disabled}
tabIndex={tabIndex}
data-outside={!cell.inCurrentMonth || undefined}
onClick={onSelect}
onFocus={onFocus}
>
{cell.day}
</button>
);
}
Grid container:
<div role="grid" aria-label="Calendar">
<div role="row">
{weekdays.map((w) => (
<div key={w} role="columnheader">
{w}
</div>
))}
</div>
{chunk(cells, 7).map((week, i) => (
<div key={i} role="row">
{week.map((cell) => (
<DayButton key={cell.date} /* ... */ />
))}
</div>
))}
</div>
Keyboard
- Maintain
focusedDateseparate fromselected - Only one day has
tabIndex={0}; others-1(roving tabindex) - ArrowLeft/Right ±1 day; ArrowUp/Down ±7; Home/End week edges
- Crossing month boundary updates visible month
Accessibility essentials
- Prev/next buttons need names:
aria-label="Previous month" - Month title can be
aria-live="polite"when month changes - Disabled days use
disablednot only visual grey - Selected day:
aria-selected="true" - Don’t rely on color alone for today vs selected
Performance notes
- Rebuild grid only when
year/month/weekStartsOnchanges (useMemo) - 42 cells is trivial; no virtualization
- Avoid creating
new Date()in render for every cell if you can precompute keys
Footguns
- Timezone off-by-one on ISO date strings
- February / leap years wrong
daysInMonth - weekStartsOn math errors leave empty first row
- Selecting outside-month day without switching visible month (confusing UX)
- Focus lost after month change — restore focus to same day number or 1st
Interview out-loud answer
I’d separate view month from selected date. A pure
buildMonthGridfills six weeks with outside-month padding. Cells are buttons in arole="grid"with roving tabindex. Selection is controlled viaYYYY-MM-DDstrings to dodge timezone bugs. MVP is paint + click select + prev/next; then min/max and keyboard. I’d useIntlfor labels if locale matters.
Related on this site
- Multi Step Wizard
- Custom Dropdown Select
- Machine Coding Interview Framework
- Accessibility Interview Talking Points