@19h47/calendar
v2.0.0
Published
Calendar
Downloads
292
Readme
@19h47/calendar
Lightweight date / range picker. Markup and CSS are yours — the package fills the grid and handles selection.
Installation
pnpm add @19h47/calendarMarkup
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 enUpdate 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