A navigable JavaScript calendar keeps a displayed year and month as state, renders that month, and changes the state when the user chooses Previous/Next Month or Previous/Next Year. Selecting a day can then copy a predictable date value such as YYYY-MM-DD into an input.
let viewYear = 2026;
let viewMonth = 8; // September: months are 0-11
function showNextMonth() {
const next = new Date(viewYear, viewMonth + 1, 1);
viewYear = next.getFullYear();
viewMonth = next.getMonth();
}This continues the earlier monthly calendar and calendar-to-input projects by adding navigation. JavaScript normalizes month overflow automatically, so moving from December to the next month also advances the year.
The historical version passed chm (change month) and chy (change year) into the calendar function. That idea is valid, but keeping the current display state in named variables makes the flow easier to follow and avoids accidental globals.
const today = new Date();
let viewYear = today.getFullYear();
let viewMonth = today.getMonth();getMonth() is zero-based: January is 0 and December is 11. See JavaScript month values for details.
Constructing a date on day 1 avoids end-of-month surprises. A month index of 12 becomes January of the next year; -1 becomes December of the previous year.
function changeMonth(amount) {
const next = new Date(viewYear, viewMonth + amount, 1);
viewYear = next.getFullYear();
viewMonth = next.getMonth();
}
function changeYear(amount) {
viewYear += amount;
}This is safer than applying setMonth() to a date currently on the 29th, 30th, or 31st because month overflow can otherwise move into a later month.
Two Date objects provide the values required for a conventional 6-row calendar grid. getDay() returns 0-6 for Sunday-Saturday, while day 0 of the following month gives the last date of the current month.
const firstDay = (new Date(viewYear, viewMonth, 1)).getDay();
const lastDate = (new Date(viewYear, viewMonth + 1, 0)).getDate();The original project used the same core calculations; the modernization changes how the UI is created, not the calendar arithmetic.
For form values, prefer an unambiguous format. Numeric MM/DD/YYYY can be misread in international contexts, while YYYY-MM-DD matches the value format used by native date inputs.
const pad = value => String(value).padStart(2, "0");
function formatDate(year, monthIndex, day) {
return `${year}-${pad(monthIndex + 1)}-${pad(day)}`;
}
input.value = formatDate(viewYear, viewMonth, selectedDay);The old project assembled long HTML strings containing inline onclick handlers. For new code, create real buttons and attach events with addEventListener(). Buttons are also a better semantic fit because month navigation and day selection perform actions rather than navigating to another URL.
const nextButton = document.createElement("button");
nextButton.type = "button";
nextButton.setAttribute("aria-label", "Next month");
nextButton.textContent = "›";
nextButton.addEventListener("click", () => {
changeMonth(1);
renderCalendar();
});Two independent inputs do not need two copies of the calendar code. Pass or store the target input and panel for each picker. The original variables bt and sub served this identification purpose; modern code can express it with data-* attributes.
<button type="button"
data-calendar-trigger
data-input="t1"
data-panel="calendar1">
Choose first date
</button>Demo: two independent calendar pickers
Use buttons for every actionable control, label the text field, expose open/closed state with aria-expanded, connect the trigger to the popup with aria-controls, and let Escape close the calendar. Disabled dates should use the native disabled state instead of color alone.
trigger.setAttribute("aria-expanded", String(opening));
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeCalendar();
});Load the JavaScript file with defer, use a normal labeled input, and keep the popup next to the trigger in the DOM. Do not rely on a clickable image as the only control.
<script src="date-project-calendar4-demo.js" defer></script>
<label for="t1">Selected date</label>
<input id="t1" name="t1" type="text" readonly>
<button id="calendarTrigger" type="button" aria-controls="sub" aria-expanded="false">Choose date</button>
<div id="sub" hidden></div>If your project only needs a date and does not require a custom calendar UI, consider native <input type="date"> first. A custom picker is justified when you need navigation behavior, constrained ranges, custom presentation, or teaching control over the rendering process.
Demo: navigable calendar with date selection
The next example extends the same approach to linked start and end date calendars, where the valid dates in one picker depend on the value selected in the other.
Date Reference All Date Projects
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.