Calendar — <mv-calendar>

Localized month calendar (Intl, week starts on the locale’s first day): single, range with hover preview, or multiple selection, min/max and disabled dates, full keyboard navigation, month/year views and a sliding transition.

CategoryData display
TypeWeb Component (<mv-calendar>)
Statusstable
Keywordscalendar, date, range, month, day-picker, intl, form-associated

When to use

Avoid when

Install

node scripts/add.mjs calendar --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["calendar"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/calendar/calendar.js, components/calendar/calendar.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:flex;flex-wrap:wrap;gap:1.5rem;justify-content:center;align-items:flex-start">
  <figure style="margin:0;display:grid;gap:.6rem;justify-items:center">
    <mv-calendar id="calendar-demo-single" value="2026-09-24" min="2026-09-01" disabled-dates="2026-09-17,2026-09-18,2026-09-29/2026-10-02" label="Appointment date"></mv-calendar>
    <figcaption id="calendar-demo-single-out" style="font-size:.8125rem;color:var(--mv-fg-muted)" aria-live="polite"></figcaption>
  </figure>
  <figure style="margin:0;display:grid;gap:.6rem;justify-items:center">
    <mv-calendar id="calendar-demo-range" mode="range" value="2026-10-05/2026-10-11" label="Stay dates"></mv-calendar>
    <figcaption id="calendar-demo-range-out" style="font-size:.8125rem;color:var(--mv-fg-muted)" aria-live="polite"></figcaption>
  </figure>
</div>
<script type="module">
  const fmt = new Intl.DateTimeFormat("en-US", { weekday: "long", day: "numeric", month: "long" });
  const rangeFmt = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "long", year: "numeric" });
  const single = document.getElementById("calendar-demo-single");
  const range = document.getElementById("calendar-demo-range");
  const singleOut = document.getElementById("calendar-demo-single-out");
  const rangeOut = document.getElementById("calendar-demo-range-out");
  const showSingle = () => {
    const d = single.valueAsDate;
    singleOut.textContent = d ? `Appointment on ${fmt.format(d)}` : "Pick a date";
  };
  const showRange = () => {
    const { start, end } = range.range;
    if (!start) rangeOut.textContent = "Pick a check-in date";
    else if (!end) rangeOut.textContent = "Pick a check-out date";
    else {
      const nights = Math.round((end - start) / 864e5);
      rangeOut.textContent = `${rangeFmt.formatRange(start, end)} · ${nights} night${nights > 1 ? "s" : ""}`;
    }
  };
  customElements.whenDefined("mv-calendar").then(() => { showSingle(); showRange(); });
  single.addEventListener("mv-change", showSingle);
  range.addEventListener("mv-change", showRange);
</script>

API

Attributes

NameTypeDefaultDescription
modesingle | range | multiplesingleSelection type.
valuestringISO: “2026-09-22”, range “2026-09-10/2026-09-18”, multiple “2026-09-01,2026-09-08”.
min / maxYYYY-MM-DDBounds: out-of-range days are disabled and navigation is blocked.
disabled-datesstringComma-separated disabled dates or ranges (“2026-09-17,2026-09-29/2026-10-02”).
disable-weekendsbooleanDisables Saturdays and Sundays.
monthsnumber1Number of months shown side by side (1–4).
monthYYYY-MMInitially displayed month (otherwise the value’s month or today’s).
localestringen-USLanguage of month and day names (Intl.DateTimeFormat) and, unless week-start is set, first day of the week.
week-start0–6localeForces the first day of the week (0 = Sunday, 1 = Monday … 6 = Saturday). Without it, the week follows locale (Sunday for en-US, Monday for en-GB or fr-FR), read from Intl.Locale week info with a built-in CLDR fallback.
todayYYYY-MM-DDOverrides today’s date (tests, time zones).
namestringForm field name (ISO value).
requiredbooleanNative validation; a range must be complete.
readonlybooleanNavigation allowed, selection frozen.
disabledbooleanDisables the whole calendar.
data-variantplainNo border or background (for use inside a popover).

Properties

NameTypeDescription
valuestringISO value (read/write; also accepts Date or Date[]).
valueAsDateDate | nullFirst selected date.
datesDate[]All selected dates, sorted.
range{ start, end }Range bounds (Date | null).
displayedMonthstringDisplayed month “YYYY-MM” (read-only).
isDateDisabled(date: Date) => booleanExtra predicate for disabled dates.

Methods

NameDescription
goTo(date, { focus })Shows the date’s month (Date or ISO), with a slide.
isDisabled(date)Whether a date (Date or ISO) cannot be selected.
focusDay()Moves focus to the active day.
checkValidity() / reportValidity()Native validation.

Events

NameDescription
mv-changedetail: { value, date, dates, start, end, complete } (complete = false while a range has no end).
mv-monthdetail: { month: "YYYY-MM" } when the displayed month changes.

CSS classes

NameDescription
mv-calendar-dayDay button: data-selected, data-today, data-outside, aria-disabled.
mv-calendar-header / -caption / -nav / -grid / -pickerGenerated parts.

CSS variables

NameDefaultDescription
--mv-calendar-cell2.25remSize of a day cell.
--mv-calendar-rangeRange band color.
--mv-calendar-radiusvar(--mv-radius-md)Radius of days and the range band.

Accessibility

APG “Date Picker Dialog” pattern (grid): role=grid table labeled by the month title (announced via a live region), weekday headers with a full abbr, a single tabbable day (roving tabindex). Arrows ±1 day / ±1 week, Page Up/Down ±1 month (Shift: ±1 year), Home/End = start/end of week, Enter/Space selects. Every day has a full aria-label (“Tuesday, September 22, 2026, today”), aria-current=date, aria-selected on the cell, aria-disabled (still focusable). Clickable title → month then year views, Esc returns to days.