calendar-wc
Standalone calendar display with events, multi-select, range picking, date constraints, form integration, multi-month view, and expressive theming.
Overview
The <calendar-wc> component displays a month grid for viewing events, availability, or annotations on dates. It supports single, range, and multi-date selection, date constraints, form integration via ElementInternals, multi-month display, and locale-aware rendering.
<calendar-wc data-events='{"2026-04-10":"Sprint review","2026-04-15":"Deadline"}' data-selection="single"></calendar-wc>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-month |
number (1-12) | current | Initial month to display. |
data-year |
number | current | Initial year to display. |
data-events |
JSON | — | Event data keyed by ISO date. Values: string, object { label, color, icon, time }, or array. |
data-selection |
string | "none" |
Selection mode: "none", "single", "range", or "multi". |
data-disabled-dates |
string | — | Comma-separated ISO dates to disable. |
data-highlight-dates |
string | — | Comma-separated ISO dates to highlight, optionally with :category. |
data-size |
string | "default" |
Size variant: "compact", "default", or "large". |
data-min-date |
ISO date | — | Earliest selectable/navigable date. |
data-max-date |
ISO date | — | Latest selectable/navigable date. |
data-months |
number (1-12) | 1 |
Number of months to display side by side. |
name |
string | — | Form field name. When set with a selection mode, the calendar participates in native form submission. |
Selection Modes
Single selection
Click a date to select it. Fires calendar:select with the selected ISO date.
<calendar-wc data-selection="single"></calendar-wc>
Range selection
Click two dates to define a range. A hover preview shows between start and cursor. The value uses ISO 8601 interval format: YYYY-MM-DD/YYYY-MM-DD. During selection, the host element gets a data-tentative attribute with the start date.
<calendar-wc data-selection="range" data-min-date="2026-04-01" data-max-date="2026-04-30"></calendar-wc>
Multi-select
Click dates to toggle them on or off. Selected dates are returned as space-separated ISO strings.
<calendar-wc data-selection="multi"></calendar-wc> <script> cal.addEventListener('calendar:select', (e) => { console.log(e.detail.values); // ["2026-04-10", "2026-04-15"] });</script>
Date Constraints
Use data-min-date and data-max-date to constrain navigation and disable out-of-range dates. Navigation buttons and the year dropdown respect these boundaries. Constraints work alongside data-disabled-dates and the isDateDisallowed callback — all three are additive.
<calendar-wc data-min-date="2026-04-01" data-max-date="2026-06-30" data-disabled-dates="2026-05-01,2026-05-25"></calendar-wc>
Multi-Month Display
Set data-months (1–12) to show multiple months in a responsive grid. Navigation shifts by one month at a time. All features (selection, events, highlights) work across all visible months.
Grid Layouts
The grid adapts its column count based on the number of months. On narrow containers, columns reduce responsively.
| data-months | Layout | Use case |
|---|---|---|
2 | 2 columns | Range picker |
3 | 3 columns | Quarter preview |
4 | 2×2 grid | Quarter view |
6 | 3×2 grid | Half-year |
12 | 4×3 grid | Yearly overview |
<!-- Range picker --><calendar-wc data-months="2" data-selection="range"></calendar-wc> <!-- Quarter view with multi-select --><calendar-wc data-months="4" data-selection="multi"></calendar-wc> <!-- Yearly overview (compact) --><calendar-wc data-months="12" data-size="compact"></calendar-wc>
Form Integration
When a name attribute is set and a selection mode is active, the calendar participates in native form submission via ElementInternals. No hidden input needed.
<form> <calendar-wc name="event-date" data-selection="single"></calendar-wc> <button type="submit">Submit</button></form> <script> form.addEventListener('submit', (e) => { e.preventDefault(); const data = new FormData(e.target); console.log(data.get('event-date')); // "2026-04-15" });</script>
JavaScript Properties
| Property | Type | Description |
|---|---|---|
.value |
string | null |
Selected value. ISO date (single), ISO interval (range), space-separated ISOs (multi), or null. |
.isDateDisallowed |
(date: Date) => boolean |
Callback to dynamically disable dates. Called per cell during render. Additive with attribute-based constraints. |
.getDayParts |
(date: Date) => string | string[] | null |
Inject custom data-day-part attributes per cell for CSS targeting. |
isDateDisallowed
const cal = document.querySelector('calendar-wc');cal.isDateDisallowed = (date) => { const day = date.getDay(); return day === 0 || day === 6; // Disable Sat/Sun};
getDayParts
Return a string or array of strings to add as data-day-part on the cell's <td>. Style with attribute selectors in CSS.
cal.getDayParts = (date) => { const parts = []; if (date.getDay() === 0 || date.getDay() === 6) parts.push('weekend'); if (date.getDate() === 15) parts.push('payday'); return parts.length ? parts : null;};
calendar-wc td[data-day-part~="weekend"] button { background: oklch(from var(--color-primary) l c h / 0.08);}calendar-wc td[data-day-part~="payday"] button { border: 2px solid var(--color-success);}
Events
| Event | Detail | Description |
|---|---|---|
calendar:select |
{ value, date } (single), { value, start, end } (range), { values, dates } (multi) |
Fired when selection changes. |
calendar:navigate |
{ year, month } |
Fired on month navigation. |
calendar:day-open |
{ date, events } |
Fired when day detail overlay opens (display-only mode). |
Size Variants
Three sizes adapt the calendar to different contexts.
| Size | Max Width | Events | Use Case |
|---|---|---|---|
compact | 16rem | Dot indicators | Sidebars, cards |
default | 22rem | Dot indicators | Standard display |
large | 56rem | Labels with icons | See Large View below |
Large View
Large mode (data-size="large") transforms the calendar into a full-page schedule view with rich event display. Instead of dot indicators, events render as labeled chips with icons and times. Container queries drive responsive cell sizing with near-square proportions.
Event Labels
In large mode, each cell shows up to 2 event chips inline. Events beyond 2 collapse into a "+N more" overflow indicator. Each chip displays the label, optional time, and optional icon.
<calendar-wc data-size="large" data-events='{ "2026-04-10": [ {"label": "Standup", "time": "09:00", "icon": "users"}, {"label": "Demo day", "time": "15:00", "icon": "presentation"}, {"label": "Team dinner", "time": "18:30", "icon": "utensils"} ]}'></calendar-wc>
Day Detail Overlay
In display-only mode (data-selection="none"), clicking a date with events opens a positioned dialog showing the full event list. If any events have a time property, an hour-grid timeline renders alongside. Close with Escape or by clicking outside.
Watermark Day Numbers
Large cells display a subtle background day number (watermark) via ::before. Control its appearance with these tokens:
| Token | Default | Purpose |
|---|---|---|
--cal-watermark-opacity | 0.05 | Background number opacity |
--cal-watermark-color | var(--color-text) | Background number color |
--cal-watermark-size | 3rem | Background number font size |
--cal-watermark-weight | 800 | Background number font weight |
Event Dots
Pass event data as JSON to show dot indicators on dates. Each date can have a string, object with label/color/icon/time, or array of those.
<calendar-wc data-events='{ "2026-04-10": "Team meeting", "2026-04-15": { "label": "Tax deadline", "color": "var(--color-error)" }, "2026-04-21": [ { "label": "Morning standup", "time": "09:00", "icon": "users" }, { "label": "Sprint review", "time": "14:00" } ]}'></calendar-wc>
Localization
Weekday headers, month names, and hour labels are rendered via Intl.DateTimeFormat, automatically matching the user's locale. Override by setting lang on the element or an ancestor.
<calendar-wc lang="fr"></calendar-wc><!-- Weekdays: lun. mar. mer. jeu. ven. sam. dim. -->
Keyboard
| Key | Action |
|---|---|
| Arrow Left / Arrow Right | Move focus one day. |
| Arrow Up / Arrow Down | Move focus one week. |
| Enter / Space | Select the focused date. |
| Escape | Close day detail overlay. |
Calendar vs Date Picker
| Component | Purpose | Form input? |
|---|---|---|
<date-picker> |
Pick a single date via popover input | Yes (ElementInternals) |
<calendar-wc> |
Display events, availability, ranges, multi-select | Yes (when name attribute is set) |
Theming
The calendar exposes 27 CSS custom properties for expressive theming — from paper wall calendars to bold geometric styles. Every token defaults to transparent or the base appearance, so they are fully opt-in. Themes set them via [data-theme~="name"] calendar-wc { ... } selectors.
Banner
| Token | Default | Purpose |
|---|---|---|
--cal-banner-height | 0px | Banner strip height (0 hides it) |
--cal-banner-bg | transparent | Background color or gradient |
--cal-banner-image | none | Background image |
--cal-banner-size | cover | Background-size |
--cal-banner-position | center | Background-position |
Header and Week Rows
| Token | Default | Purpose |
|---|---|---|
--cal-header-bg | var(--color-surface-raised) | Navigation header background |
--cal-header-color | inherit | Navigation header text |
--cal-week-bg | transparent | Week row background |
--cal-week-bg-alt | transparent | Alternating even-row tint |
Day Cells
| Token | Default | Purpose |
|---|---|---|
--cal-day-bg | transparent | Day cell background |
--cal-day-image | none | Decoration layer image (stickers) |
--cal-day-image-size | contain | Decoration background-size |
--cal-day-image-opacity | 1 | Decoration opacity |
Today, Disabled, Outside
| Token | Default | Purpose |
|---|---|---|
--cal-today-bg | var(--color-surface-raised) | Today cell background |
--cal-today-border | var(--color-border-strong) | Today cell border |
--cal-today-shadow | none | Today cell glow or shadow |
--cal-disabled-opacity | 0.5 | Disabled day opacity |
--cal-outside-opacity | 0.35 | Outside-month day opacity |
--cal-outside-color | var(--color-text-muted) | Outside-month text color |
Highlight
| Token | Default | Purpose |
|---|---|---|
--cal-highlight-bg | interactive tint | Highlighted date background |
--cal-highlight-border | interactive tint | Highlighted date border |
Accessibility
The grid uses role="grid" with individual day buttons. Today is marked with aria-current="date", selected dates with aria-selected="true", and disabled dates with aria-disabled="true". The month header uses aria-live="polite" for navigation announcements. Arrow keys navigate the grid; Enter/Space selects.
Related
<date-picker>— Popover date input for forms