Calendar
A lightweight date / range picker. Markup and CSS are yours. The package fills
the grid and handles selection. <cinq-calendar> follows the
WAI-ARIA Authoring Practices date picker grid.
Inspired by @19h47/calendar.
Install
Section titled “Install”pnpm add @agencecinq/calendarImport once:
import "@agencecinq/calendar";Then write the grid markup. The host <cinq-calendar> fills weekday headers and
day cells, and wires selection + keyboard navigation.
HTML is the source of truth. The component does not invent missing ARIA attributes. Provide
role="grid", labels, and live regions yourself.
<cinq-calendar locale="fr" deselect> <header> <button type="button" class="js-previous" aria-label="Previous month">Previous</button> <button type="button" class="js-title" id="calendar-label" aria-live="polite"></button> <button type="button" class="js-next" aria-label="Next month">Next</button> </header> <table role="grid" aria-labelledby="calendar-label"> <thead> <tr class="js-days"></tr> </thead> <tbody class="js-body"></tbody> </table></cinq-calendar>The component upgrades automatically on connectedCallback.
Examples
Section titled “Examples”Interactive demos below cover single date, range, multiple dates, preselection,
future-only, locale / week start, and an advanced renderInner events calendar.
Single date
Locale fr, deselect and allow-past. Click the selected day again to clear. Try the keyboard once a day is focused: arrows, Home/End, Page Up/Page Down.
Selection : Aucune date
Date range
mode="range". Pick a start day, then an end day. Hover (or move focus) paints the in-between preview. A third click resets the range.
Selection : Aucune date
Multiple dates
mode="multiple". Toggle discrete days in and out of the selection. No range painting. Click a selected day again to remove it. Cap the count in your app if needed (no built-in max).
Selection : Aucune date
Preselected dates
Seed the view with data-month, data-year, and data-picked. Here a range covering mid-November 2025.
Selection : 2025-11-10 to 2025-11-18
Future only
Without allow-past, days before today are aria-disabled and cannot be selected (still in the keyboard grid).
Selection : Aucune date
Locale & week start
Change the locale. Labels update via Intl, and week start follows getWeekStart(locale) unless you override first-day.
firstDay = 1(0=Sun ... 6=Sat)
Selection : Aucune date
Advanced: event markers
Override renderInner to wrap each day in <time>, mark weekends, and paint dots on days that have events. Only event days are selectable. Others get aria-disabled.
Selection : Aucune date
Single date
Section titled “Single date”Default mode="single". Add deselect to clear by clicking the selected day again.
<cinq-calendar locale="fr" deselect allow-past name="arrival"> <!-- header + table --></cinq-calendar>Date range
Section titled “Date range”Set mode="range". First click is the start, second is the end. Hover or
keyboard focus previews the range. A third click resets.
<cinq-calendar locale="fr" mode="range" allow-past name="stay"> <!-- header + table --></cinq-calendar>Multiple dates
Section titled “Multiple dates”Set mode="multiple". Each click toggles a day in or out of the selection. No
range preview.
<cinq-calendar locale="fr" mode="multiple" allow-past name="availability"> <!-- header + table --></cinq-calendar>The package does not enforce a maximum number of days. That is application
logic. Listen to calendar:change and trim picked yourself. Pass
emit: false to setPicked so the trim does not fire a second
calendar:change:
import { EVENTS } from "@agencecinq/utils";
const MAX = 3;
el.addEventListener(EVENTS.CALENDAR_CHANGE, () => { if (el.picked.length <= MAX) return;
el.setPicked(el.picked.slice(0, MAX), false); el.render();});Preselected dates
Section titled “Preselected dates”Seed the viewed month and selection from attributes:
<cinq-calendar locale="fr" mode="range" allow-past data-month="10" data-year="2025" data-picked='["2025-11-10","2025-11-18"]'> <!-- header + table --></cinq-calendar>Future only
Section titled “Future only”Omit allow-past (default). Past days stay in the grid with
aria-disabled="true" and cannot be selected.
<cinq-calendar locale="fr" deselect> <!-- header + table --></cinq-calendar>Locale & week start
Section titled “Locale & week start”locale drives labels via Intl. Week start follows the locale. Override with
first-day (0=Sun … 6=Sat). Use the select in the playground to try
en, fr, de, es, ja, and ar.
<!-- French to week starts Monday --><cinq-calendar locale="fr">...</cinq-calendar>
<!-- English labels, force Monday --><cinq-calendar locale="en" first-day="1">...</cinq-calendar>Update at runtime:
import { getWeekStart } from "@agencecinq/calendar";
const el = document.querySelector("cinq-calendar");el.options.locale = "ja";el.options.firstDay = getWeekStart("ja");el.render();Cell hook
Section titled “Cell hook”Override renderInner to enrich each day cell. Keep .js-day and data-day.
The playground Advanced: event markers example uses it to add <time>,
weekend styling, event dots, and to disable days without events.
el.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>`;};Required markup
Section titled “Required markup”| Selector | Required | Role |
|---|---|---|
<cinq-calendar> |
Yes | Host element |
.js-body |
Yes | Day cells container (tbody) |
.js-days |
Yes | Weekday headers row |
.js-previous / .js-next |
Optional | Month navigation |
.js-title |
Optional | Month / year label (click = next month) |
.js-day |
Generated | Day button |
Attributes
Section titled “Attributes”| Attribute | Description |
|---|---|
locale / data-locale |
BCP 47 locale for labels |
mode |
single (default), range, or multiple |
deselect |
Allow clearing selection in mode="single" |
allow-past |
Allow selecting past dates |
first-day |
Week start (0=Sun … 6=Sat) |
button-class |
Extra classes on day buttons |
name |
Forwarded in change event detail |
data-month / data-year |
Initial view (month is 0-11) |
data-picked |
JSON array of preselected YYYY-MM-DD days |
Events
Section titled “Events”| Event | Constant | Detail |
|---|---|---|
calendar:change |
CALENDAR_CHANGE |
{ values: string[], name?: string } |
import { EVENTS } from "@agencecinq/utils";
document.querySelector("cinq-calendar")?.addEventListener( EVENTS.CALENDAR_CHANGE, ({ detail }) => console.log(detail.values, detail.name),);Keyboard
Section titled “Keyboard”| Key | Action |
|---|---|
| Arrow keys | Move by day / week |
| Home / End | First / last day of the week |
| Page Up / Page Down | Previous / next month |
| Shift + Page Up / Down | Previous / next year |
| Enter / Space | Select the focused day |
| Method / property | Description |
|---|---|
options |
Runtime options object |
picked |
Selected days as YYYY-MM-DD |
current |
Viewed { month, year, day } |
render() |
Rebuild the grid |
setPicked(picked, emit?) |
Assign selection, mirror data-picked. Emits calendar:change when emit is true (default). Pass false to sync without re-firing the event. |
move(delta) |
Navigate by months (-1 = previous, 1 = next) |
destroy() |
Detach listeners and clear the grid |
renderInner(inner, date) |
Per-cell hook (override) |
Exported helpers: getWeekStart, toDayString, fromDayString.