@19h47/calendar
Lightweight date / range picker. Markup and CSS are yours — the package fills the grid and handles selection.
Installation
pnpm add @19h47/calendar
Markup
Required hooks:
| Selector | Role |
|---|---|
| Root element | Passed to new Calendar(el, options) |
.js-previous / .js-next |
Month navigation (optional) |
.js-title |
Month / year label; click advances to the next month (same as .js-next) |
.js-days |
Weekday headers row |
.js-body |
Day cells |
.js-day |
Day button (generated) |
<div class="Calendar js-calendar">
<header>
<button type="button" class="js-previous">Previous</button>
<button type="button" class="js-title" id="calendar-label" aria-live="polite"></button>
<button type="button" class="js-next">Next</button>
</header>
<table role="grid" aria-labelledby="calendar-label">
<thead>
<tr class="js-days"></tr>
</thead>
<tbody class="js-body"></tbody>
</table>
</div>
Usage
import Calendar, { type Options } from "@19h47/calendar";
const el = document.querySelector(".js-calendar") as HTMLElement;
const options: Partial<Options> = {
locale: "fr",
deselect: true,
buttonClass: "btn btn-outline-primary", // optional — lib ships unstyled buttons
};
const calendar = new Calendar(el, options);
calendar.init();
el.addEventListener("Calendar.change", ({ detail }) => {
// detail.values → string[] (`YYYY-MM-DD`)
console.log(detail.values);
});
destroy() detaches listeners and clears the grid.
Single date
new Calendar(el, { single: true, deselect: true }).init();
Date range
new Calendar(el, { single: false }).init();
Locale & week start
locale drives labels via Intl. Week start defaults from the locale (weekInfo); pass firstDay to override.
new Calendar(el, { locale: "fr" }).init();
// fr → week starts Monday
new Calendar(el, { locale: "en", firstDay: 1 }).init();
// force Monday despite en
Update at runtime:
import Calendar, { getWeekStart } from "@19h47/calendar";
calendar.options.locale = "ja";
calendar.options.firstDay = getWeekStart("ja");
calendar.render();
Custom labels
new Calendar(el, {
locale: "fr",
days: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
months: [
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre",
],
}).init();
Cell hook
Override renderInner to enrich or rebuild each day cell. Keep .js-day and data-day.
calendar.renderInner = (inner, date) => {
const button = inner.querySelector(".js-day");
if (!button) return;
button.classList.add("MyDay");
button.innerHTML = `<time datetime="${date.toISOString().slice(0, 10)}">${date.getDate()}</time>`;
};
Pair with custom stateClasses / header markup — see the Custom markup demo in index.html.
Options
| Option | Type | Default | Description |
|---|---|---|---|
locale |
string |
document.documentElement.lang or "en" |
BCP 47 locale for title and weekday/month labels |
buttonClass |
string |
"" |
Extra classes on day buttons (e.g. Bootstrap) |
months |
string[] |
— | Month names override (Jan→Dec). Defaults to Intl |
days |
string[] |
— | Weekday labels override (Sun→Sat). Defaults to Intl |
single |
boolean |
true |
Single date vs range |
firstDay |
number |
from locale | Week start (0=Sun … 6=Sat) |
deselect |
boolean |
false |
Allow clearing the selection in single mode |
allowPast |
boolean |
false |
Allow selecting past dates |
name |
string |
— | Forwarded in Calendar.change detail |
stateClasses |
object |
see source | Selection classes (active, range, start, end) |
Exported types: Options, StateClasses, Current. Also exported: getWeekStart, toDayString, fromDayString.
Attributes
| Attribute | Description |
|---|---|
data-month |
Initial month (0–11) |
data-year |
Initial year |
data-picked-dates |
JSON array of YYYY-MM-DD days to preselect, e.g. ["2023-11-05","2023-11-07"] |
Accessibility
The calendar grid follows the WAI-ARIA APG date grid practices.
In your markup (source of truth): role="grid", aria-labelledby, aria-live on the title, aria-label on nav buttons, etc.
Generated by the lib:
- Roving
tabindex(one focusable day at a time) aria-selectedon selected daysaria-disabledon unavailable days (still in the keyboard grid)aria-current="date"on today- Weekday headers with
abbr(full day name)
Keyboard (focus in the grid)
| Key | Action |
|---|---|
| Arrow keys | Move by day / week (crosses months) |
| Home / End | First / last day of the week |
| Page Up / Page Down | Previous / next month (same day number, or last day of month) |
| Shift + Page Up / Down | Previous / next year |
| Enter / Space | Select the focused day |
Events
Calendar.change — fired on selection change.
detail: {
values: string[]; // `YYYY-MM-DD`
name?: string;
}
Live demos
- GitHub Pages (from
docs/) - Local:
pnpm devthen open the app (seeindex.html)
Refresh the Pages demo after lib changes:
pnpm run docs