npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

intl-datepicker

v0.4.1

Published

Framework-agnostic multi-calendar datepicker Web Component powered by Intl.DateTimeFormat

Readme

intl-datepicker

npm version CI license

Live demo → · npm package

Status: pre-1.0 — usable and tested. Per semver, breaking API changes remain possible on minor bumps until 1.0; each one is listed in the CHANGELOG. If you spot a rough edge, please file an issue.

A framework-agnostic, multi-calendar datepicker Web Component powered by Intl.DateTimeFormat.

  • No time-zone bugs — values are plain ISO dates ("2026-03-15") with no time or zone, so a date never shifts by a day between browser, server and database. Details
  • 14 calendar systems — Gregorian, Persian, Islamic (3 variants), Hebrew, Buddhist, Japanese, Indian, Ethiopic, Coptic, ROC, and more
  • Locale-aware — month/day names, digits, first day of the week, weekend days and RTL all come from Intl; typed input accepts native digits (۱۴۰۳/۰۵/۱۲)
  • Built-in label translations for English (default), with opt-in entry points for Persian, Arabic, and Hebrew. Other locales fall back to English; supply your own via the labels API
  • Multiple picker types — date, range, week, multiple, month, year
  • Zero-framework lock-in — works with vanilla HTML, React, Vue, Svelte, Angular
  • SSR-safe — importable in Node/Next.js without crashing; rendering is still client-only
  • Form-associated — participates in <form> submission, validation, reset and <fieldset disabled>
  • Accessible — WAI-ARIA grid with labelled column headers, live-region announcements, full keyboard support, <label for> support, forced-colors and reduced-motion support
  • Never clipped — the popup opens in the browser's top layer, above overflow: hidden, transforms, z-index stacks and modal <dialog>s

Install

npm install intl-datepicker

Quick Start

<script type="module">
  import 'intl-datepicker';
</script>

<intl-datepicker></intl-datepicker>

Without a bundler (CDN)

<script type="module" src="https://esm.sh/[email protected]/full"></script>

<intl-datepicker calendar="persian" locale="fa-IR"></intl-datepicker>

/full registers every calendar and label set in one module, which is the simplest choice from a CDN.

Non-Gregorian Calendars

Non-Gregorian calendars must be explicitly imported (they are tree-shakeable):

import 'intl-datepicker';
import 'intl-datepicker/calendars/persian';
import 'intl-datepicker/labels/fa'; // Persian UI strings (optional)
<intl-datepicker calendar="persian" locale="fa-IR"></intl-datepicker>

Or import all 14 calendars at once:

import 'intl-datepicker/full'; // includes all calendar systems and label sets

Locale Labels

English labels ship with the main bundle. Persian, Arabic, and Hebrew label sets are tree-shakeable — import the ones you need:

import 'intl-datepicker';
import 'intl-datepicker/labels/fa';
<intl-datepicker locale="fa-IR"></intl-datepicker>

For any locale without a built-in set (or to override individual strings), pass a labels object via the attribute or property API:

<intl-datepicker labels='{"today": "Now", "dateTooEarly": "Pick {date} or later"}'></intl-datepicker>

See IntlDatepickerLabels in the type declarations for every key. Keys with placeholders: rangeSelected ({start}, {end}), formatHint and invalidDate ({format}, {example}), dateTooEarly/dateTooLate ({date}), rangeTooShort/rangeTooLong/minNightsHint/maxNightsHint ({nights}).

nights is a plural label: a string, or forms keyed by Intl.PluralRules category. {n} is printed in the picker's numerals:

picker.labels = { nights: { one: '{n} nuit', other: '{n} nuits' } };
// Arabic ships zero/one/two/few/many/other: "ليلة واحدة", "ليلتان", "٣ ليالٍ"

Values & time zones

The picker never deals in time zones. Every value is a calendar date with no time attached, written in ISO 8601 (Gregorian), whatever calendar the user sees:

| type | Value | Example | |---|---|---| | date | YYYY-MM-DD | 2026-03-15 | | range | start/end | 2026-03-15/2026-03-20 | | week | ISO week | 2026-W11 | | multiple | comma-separated dates | 2026-03-15,2026-03-18 | | month | Gregorian: YYYY-MM · other calendars: see below | 2026-03 | | year | Gregorian: YYYY · other calendars: see below | 2026 |

What to store on the server: the value string as-is, in a DATE column (or daterange for ranges). Don't convert it to a Date/timestamp — that is what introduces the off-by-one-day bugs. valueAsDate exists for convenience and returns local midnight.

"Today" (disable-past, disable-future, the Today button, relative presets) is the date in the browser's time zone, and it moves at local midnight even on a calendar that stays open. A business cutoff in another time zone ("bookings close at 18:00 New York time") should come from the server as min.

Programmatic values are kept

Like a native <input>, any value that parses is kept and displayed, even on a disabled day, outside min/max or breaking the range rules. Editing an old record with disable-past shows its past date instead of wiping it. The problem is reported through validity, in this order:

| Problem | validity flag | Message label | |---|---|---| | Unreadable typed input | badInput | invalidDate / dateUnavailable | | required and empty | valueMissing | pleaseSelectDate | | required range with only a start | valueMissing | rangeIncomplete | | Before min / after max | rangeUnderflow / rangeOverflow | dateTooEarly / dateTooLate | | On a disabled day (or a range across one, see below) | customError | dateUnavailable / rangeUnavailable | | Range shorter than min-nights / longer than max-nights | tooShort / tooLong | rangeTooShort / rangeTooLong |

Only user interaction is restricted: people can't pick an invalid date or range.

Month and year values in non-Gregorian calendars

A Persian, Hijri or Hebrew month doesn't line up with a Gregorian month, so "2024-07" can't name one. Month and year values follow the JavaScript standard Temporal.PlainYearMonth format instead: the ISO date of the period's first day plus a calendar tag.

<intl-datepicker type="month" calendar="persian" value="2024-07-22[u-ca=persian]"></intl-datepicker>
  • The first 10 characters are a normal ISO date, so any backend can parse them.
  • Temporal.PlainYearMonth.from(value) gives you Mordad 1403 directly.
  • type="year" uses the first day of the year: "2024-03-20[u-ca=persian]" is 1403.
  • value, min, max and setValue() also accept any plain ISO date and snap to the month (or year) containing it: setValue('2024-08-10') selects Mordad 1403.
  • The short YYYY-MM / YYYY forms are only accepted for calendar="gregory".

The intl-change detail (and getValue()) gives you all three shapes you are likely to need:

{
  type: 'month',
  value: '2024-07-22[u-ca=persian]',
  calendar: { year: 1403, month: 5 },   // native numbers, e.g. a payroll key
  start: '2024-07-22',                   // Gregorian bounds, for range queries
  end: '2024-08-21',
  formatted: 'مرداد ۱۴۰۳',
}

Picker Types

Single Date (default)

<intl-datepicker value="2026-03-15"></intl-datepicker>

Date Range

<intl-datepicker type="range" min="2026-01-01" max="2026-12-31"></intl-datepicker>

Week Picker

<intl-datepicker type="week"></intl-datepicker>

The value is the ISO week (Monday-based). The selection itself follows the locale's week, or first-day-of-week, so start/end in the event detail are authoritative. The ISO value names the ISO week containing the selection's Thursday, which is what makes it round-trip for every first day: a week that starts on Wednesday, Dec 30 is "…-W53" or "…-W01" depending on where that Thursday falls. An ISO week in min/max covers its whole locale week.

Multiple Dates

<intl-datepicker type="multiple" max-dates="5" sort-dates></intl-datepicker>

Month Picker

<intl-datepicker type="month"></intl-datepicker>

Year Picker

<intl-datepicker type="year"></intl-datepicker>

Range Rules

For type="range", length is counted in nights: end − start.

<!-- Hotel: 2–28 nights, check-out may be on someone else's check-in day -->
<intl-datepicker type="range" min-nights="2" max-nights="28" exclude-disabled="nights"
  disabled-dates='["2026-10-20/2026-10-22"]' disable-past></intl-datepicker>

| Attribute | Meaning | |---|---| | min-nights | Shortest range. Unset or 0 allows start = end (one day); 1 forbids it | | max-nights | Longest range | | exclude-disabled (bare, or "days") | No disabled day anywhere in [start, end] | | exclude-disabled="nights" | No disabled day in [start, end − 1]: the end may be the first disabled day |

Without exclude-disabled, a range may span disabled days (weekends, holidays in a leave request); only its start and end must be selectable.

| Booked nights 12–13 | days | nights | |---|---|---| | 10 → 11 | ✓ | ✓ | | 10 → 12 (check out the morning someone checks in) | ✗ | ✓ | | 10 → 14 | ✗ | ✗ | | 12 → 14 (check in on a booked night) | ✗ | ✗ |

After the first click:

  • Days that can't end a valid range get aria-disabled and are skipped by the hover preview, which therefore stops at the first booked night. In "nights" mode the first disabled day after the start stays selectable as a check-out day.
  • The start itself is never blocked: clicking it again (or Enter) clears it when a one-day range isn't allowed.
  • The footer shows the limits (part="range-hint", e.g. "Minimum stay: 2 nights · Maximum: 28 nights"), and the same text is announced.
  • Enter on a blocked day selects nothing and announces why ("Choose at least 2 nights").
  • Ranges are evaluated in sorted order, so clicking before the start swaps them.
  • mapDays receives isRangeBlocked and isCheckoutOnly for styling, e.g. a strike-through on booked days.

Presets that resolve to a range breaking these rules are disabled, never shortened to fit. Days force-disabled by mapDays don't count for exclude-disabled; use disabled-dates or disabledDatesFilter for availability. "Check-in only" days aren't supported.

Attributes

| Attribute | Type | Description | |---|---|---| | calendar | string | Calendar system (see table below). Default: "gregory" | | locale | string | BCP 47 locale tag. Default: <html lang>, then the browser language | | numerals | string | Numbering system override, e.g. latn for 0–9 in fa-IR, arab for Arabic-Indic | | value | string | Initial value (see Values & time zones) | | type | string | Picker type: date, range, week, multiple, month, year | | min | string | Earliest selectable value, same format as value | | max | string | Latest selectable value, same format as value | | for | string | ID of an external <input> to bind to | | placeholder | string | Input placeholder text | | name | string | Form field name | | inline | boolean | Always-visible calendar (no popup) | | disabled | boolean | Disable the picker | | readonly | boolean | Read-only input | | required | boolean | Mark as required for form validation | | show-alternate | boolean | Show the Gregorian equivalent below the calendar | | disabled-dates | string | JSON array of ISO dates and inclusive ranges to disable, e.g. '["2026-01-01","2026-12-20/2027-01-05"]' | | disable-weekends | boolean | Disable the locale's weekend days (Sat–Sun in en-US, Fri in fa-IR, Fri–Sat in ar-SA) | | disabled-days-of-week | string | Weekdays to disable: "5,6" (0 = Sunday) or "fri,sat". Combines with disable-weekends | | disable-past | boolean | Disable days before today; for week/month/year, periods before the current one | | disable-future | boolean | Disable days after today; for week/month/year, periods after the current one | | first-day-of-week | string | 0–6 (0 = Sunday) or sun…sat. Default: the locale's | | min-nights | number | Range: minimum nights (see Range Rules) | | max-nights | number | Range: maximum nights | | exclude-disabled | string | Range: no disabled days inside; "nights" allows check-out on one | | date-separator | string | Separator for multiple date display. Default: ", " | | max-dates | number | Max dates selectable in multiple mode | | sort-dates | boolean | Auto-sort selected dates in multiple mode | | months | number | Number of side-by-side month panels (1–3) | | presets | string | JSON array of range presets (see below) | | no-animation | boolean | Disable open/close animations | | show-week-numbers | boolean | Show week numbers using the locale's week rules: first day, and how many days of January week 1 needs (CLDR: 4 in most of Europe, 1 elsewhere) | | hide-outside-days | boolean | Hide days from adjacent months | | fixed-weeks | boolean | Always render six weeks so the height never changes | | caption-layout | string | Header layout: button (default), dropdown, dropdown-months, dropdown-years | | allow-input | boolean | Allow typing dates into the input (shows a format hint and inline errors) | | date-format | string | Segment order for typed input: auto (default, from the locale), YMD, DMY, MDY | | labels | string | JSON object overriding UI strings (see Locale Labels) |

Supported Calendars

| calendar value | System | |---|---| | gregory | Gregorian (default) | | persian | Persian (Solar Hijri / Jalali) | | islamic | Islamic (Umm al-Qura) | | islamic-umalqura | Islamic (Umm al-Qura) | | islamic-civil | Islamic (Civil/Tabular) | | islamic-tbla | Islamic (Tabular) | | hebrew | Hebrew | | buddhist | Buddhist | | japanese | Japanese | | indian | Indian National | | ethiopic | Ethiopic | | ethioaa | Ethiopic (Amete Alem) | | coptic | Coptic | | roc | ROC (Minguo/Taiwan) |

Persian leap years follow the astronomical calendar used in Iran (1403 is a leap year; Esfand 30, 1403 = 2025-03-20), not the 33-year arithmetic rule.

Events

| Event | detail | Description | |---|---|---| | intl-select | SelectDetail | Fired when the user picks a date (click, keyboard, Today, preset, typed input) | | intl-change | SelectDetail | Fired whenever the value changes, including setValue()/clear(). Setting the same value again does not fire | | intl-navigate | { year, month, direction, start, end } | Fired when the user changes the visible month: buttons, dropdowns, the month/year views, keyboard, or Today. year/month are the first visible month in the active calendar; start/end are the Gregorian ISO bounds of all visible months | | intl-open | — | Cancelable. Fired before popup opens | | intl-close | — | Cancelable. Fired before popup closes |

SelectDetail Shape

The detail shape depends on the picker type. { year, month, day } objects are in the active calendar; value, and start/end for month/year, are ISO.

// type="date"
{ type, value, calendar: { year, month, day }, formatted }

// type="month" | "year"
{ type, value, calendar: { year, month } | { year }, start: 'YYYY-MM-DD', end: 'YYYY-MM-DD', formatted }

// type="range" | "week"
{ type, value, start: { year, month, day }, end: { year, month, day }, formatted }

// type="multiple"
{ type, value, dates: [{ year, month, day }, ...], formatted }

JavaScript API

const picker = document.querySelector('intl-datepicker');

// Properties
picker.value;           // value string (see "Values & time zones")
picker.type;            // 'date' | 'range' | … (reflects the attribute)
picker.valueAsDate;     // native Date (local midnight) or null
picker.displayValue;    // formatted display string
picker.calendarValue;   // CalendarDate object
picker.rangeStart;      // ISO string or null (range/week)
picker.rangeEnd;        // ISO string or null (range/week)
picker.selectedDates;   // CalendarDate[] (multiple)

// Methods
picker.getValue();              // full SelectDetail or null
picker.setValue('2026-04-05');  // set value programmatically
picker.clear();                 // clear selection
picker.open();                  // open popup
picker.close();                 // close popup
picker.goToMonth(2026, 6);      // navigate to a specific month (active calendar)

// Callbacks (set via JS only)
picker.mapDays = ({ date, isToday, isDisabled }) => {
  if (date.dayOfWeek === 5) return { className: 'friday', content: '🎉' };
};

picker.disabledDatesFilter = ({ year, month, day, dayOfWeek, iso }) => {
  return day === 13; // disable all 13ths
};

The filter and mapDays get the day in the active calendar (year, month, day), its Gregorian iso date ("2026-03-21") for matching backend data, and dayOfWeek from 0 (Sunday) to 6, whatever first-day-of-week is.

presets and labels accept either an array/object or the same JSON string as the attribute.

Range Presets

<intl-datepicker
  type="range"
  presets='[
    {"label": "Last 7 days", "value": "-6d/today"},
    {"label": "This month", "value": "monthStart/monthEnd"},
    {"label": "Last month", "value": "prevMonthStart/prevMonthEnd"},
    {"label": "This year", "value": "yearStart/today"}
  ]'
></intl-datepicker>

Preset value is start/end, each one of:

  • today
  • -Nd / +Nd — N days before/after today
  • monthStart / monthEnd — this month
  • prevMonthStart / prevMonthEnd — last month
  • yearStart / yearEnd — this year
  • an ISO date, e.g. 2026-01-01

Months and years are computed in the active calendar: with calendar="persian", "This month" is the current Persian month. Results are clamped to min/max (so "This month" with disable-future ends today). A preset that still breaks the range rules, such as "Last 90 days" with max-nights="30", is disabled.

Presets can also be set via JavaScript:

picker.presets = [
  { label: 'This week', value: '-6d/today' },
  { label: 'This month', value: 'monthStart/monthEnd' },
];

Custom Day Rendering (mapDays)

picker.mapDays = (info) => {
  // info: { date, isToday, isSelected, isDisabled, isInRange,
  //         isRangeStart, isRangeEnd, isRangeBlocked, isCheckoutOnly,
  //         isCurrentMonth }
  // date: { year, month, day, dayOfWeek, iso }  (active calendar + ISO)

  return {
    className: 'my-class',     // extra CSS class
    style: 'color: red',       // inline style
    content: '<span>!</span>', // HTML appended inside the cell (trusted HTML only)
    disabled: true,            // force-disable this day
    hidden: true,              // hide this cell
    title: 'Tooltip text',     // title attribute
  };
};

Recipes

Hotel or rental booking

<intl-datepicker type="range" name="stay" months="2" required disable-past
  min-nights="1" max-nights="28" exclude-disabled="nights"
  disabled-dates='["2026-10-20/2026-10-22","2026-11-03"]'></intl-datepicker>

disabled-dates lists booked nights. Check-in can't be on one; check-out can be on the first one, since that guest leaves in the morning. Once a check-in is picked, days past the next booked night are blocked and the minimum and maximum stay are shown. A value with only a check-in fails required ("Select an end date").

Store start and end from the value ("2026-10-03/2026-10-07") as two DATE columns. Nights = days between them; no time zone math involved.

Availability from an API

Load the visible months' availability as the user navigates. intl-navigate gives the visible window as Gregorian start/end, and the filter gets each day's Gregorian iso, so a Persian or Hijri page queries and matches the same backend data. A pending check-in survives the update:

const picker = document.querySelector('intl-datepicker');
const booked = new Set();

async function loadAvailability(start, end) {
  const res = await fetch(`/api/booked-nights?from=${start}&to=${end}`); // ["2026-10-20", …]
  for (const iso of await res.json()) booked.add(iso);
  // Assigning the filter re-renders with the new data.
  picker.disabledDatesFilter = ({ iso }) => booked.has(iso);
}

picker.addEventListener('intl-navigate', ({ detail }) => loadAvailability(detail.start, detail.end));
loadAvailability('2026-10-01', '2026-11-30'); // the initially visible months

Or set ranges directly: picker.setAttribute('disabled-dates', JSON.stringify(['2026-10-20/2026-10-22'])).

Leave or vacation request

<intl-datepicker type="range" name="leave" disable-weekends
  disabled-dates='["2026-12-24/2026-12-26","2027-01-01"]'></intl-datepicker>

No exclude-disabled: the request may span weekends and holidays, but can't start or end on one.

Clinic appointment

<intl-datepicker name="visit" disable-past disabled-days-of-week="fri"
  disabled-dates='["2026-12-20/2027-01-05"]'></intl-datepicker>

Payroll month (Persian, Hijri, …)

<intl-datepicker type="month" calendar="persian" locale="fa-IR" name="period"></intl-datepicker>
picker.addEventListener('intl-change', ({ detail }) => {
  const key = `${detail.calendar.year}-${detail.calendar.month}`; // "1403-5"
  fetch(`/payroll?from=${detail.start}&to=${detail.end}`);         // Gregorian bounds
});

Reports with presets

<intl-datepicker type="range" max-nights="365" disable-future
  presets='[{"label":"Last 30 days","value":"-29d/today"},{"label":"This year","value":"yearStart/today"}]'>
</intl-datepicker>
<intl-datepicker type="range" calendar="persian" locale="fa-IR" max="2026-12-31"
  presets='[{"label":"این ماه","value":"monthStart/today"},{"label":"ماه قبل","value":"prevMonthStart/prevMonthEnd"}]'>
</intl-datepicker>

Birth date

Scrolling back decades in a calendar is slow. Let people type, and give them year and month dropdowns:

<label for="dob">Date of birth</label>
<intl-datepicker id="dob" name="dob" allow-input caption-layout="dropdown"
  min="1900-01-01" disable-future required></intl-datepicker>

For a card expiry, <intl-datepicker type="month" disable-past> keeps the current month valid.

allow-input shows the expected format under the field (e.g. Format: MM/DD/YYYY), accepts native digits and compact entry (06171990), and shows a persistent error for anything it can't read.

Hebrew and Gregorian on the same form

<intl-datepicker calendar="hebrew" locale="he-IL" show-alternate></intl-datepicker>

The user picks in the Hebrew calendar and sees the Gregorian date underneath; the submitted value is still ISO.

CSS Custom Properties

Style the component from the outside:

intl-datepicker {
  --idp-primary: #2563eb;
  --idp-bg: #ffffff;
  --idp-text: #1f2937;
  --idp-border: #d1d5db;
  --idp-hover: #f3f4f6;
  --idp-selected-bg: var(--idp-primary);
  --idp-selected-text: #ffffff;
  --idp-today-border: var(--idp-primary);
  --idp-disabled: #9ca3af;
  --idp-error: #dc2626;
  --idp-radius: 8px;
  --idp-day-size: 40px;           /* never rendered below 24px */
  --idp-font-size: 14px;
  --idp-font-family: system-ui, -apple-system, sans-serif;
  --idp-range-bg: #dbeafe;
  --idp-range-text: var(--idp-text);
  --idp-muted: #6b7280;
  --idp-z-index: 1000;            /* only used without Popover API support */
  --idp-input-min-width: 200px;
  --idp-calendar-min-width: 300px;
}

CSS Shadow Parts

Use ::part() for deeper styling:

intl-datepicker::part(input) { border-radius: 12px; }
intl-datepicker::part(calendar) { box-shadow: 0 8px 24px rgba(0,0,0,0.15); }
intl-datepicker::part(day) { border-radius: 50%; }
intl-datepicker::part(header) { background: #f0f0f0; }

| Part | Element | |---|---| | input-wrapper | Input container | | input | The <input> element | | hint | Format hint under the input (allow-input) | | error | Error message for unreadable typed input | | calendar | Calendar panel | | header | Month/year header bar | | header-title | Header title area | | nav-prev | Previous navigation button | | nav-next | Next navigation button | | month-dropdown | Month <select> (caption-layout) | | year-dropdown | Year <select> (caption-layout) | | weekday | Weekday column header | | day | Day cell button | | month-cell | Month cell (month picker view) | | year-cell | Year cell (year picker view) | | footer | Footer bar with Today/Clear | | today-btn | "Today" button | | clear-btn | "Clear" button | | alternate | Gregorian alternate display | | presets | Presets sidebar | | range-hint | Minimum/maximum nights while a range start is pending |

A Custom Elements Manifest ships at dist/custom-elements.json (linked from package.json), so editors and Storybook pick up attributes, events, parts and CSS properties.

External Input Binding

Bind the picker to any existing input:

<input type="text" id="my-input" placeholder="Pick a date">
<intl-datepicker for="my-input" calendar="persian" locale="fa-IR"></intl-datepicker>

Form Integration

<form>
  <label for="birthday">Birthday</label>
  <intl-datepicker id="birthday" name="birthday" required min="1950-01-01" max="2010-12-31"></intl-datepicker>
  <button type="submit">Submit</button>
</form>

The component participates in native form submission, validation (required, min/max, disabled days, range rules, unreadable typed input), form.reset(), and <fieldset disabled>. See Programmatic values are kept for the validity flags. Validation messages come from the labels and are localized for fa, ar and he.

Accessibility

  • Each month is a <table role="grid"> named by its month heading, with <th scope="col"> weekday headers that carry the full day name.
  • Today has aria-current="date"; selected days and range ends say so in their name.
  • Month changes from the navigation buttons and every selection are announced through one polite live region.
  • A <label for> or aria-label on <intl-datepicker> names the inner input.
  • Navigation buttons at min/max stay focusable with aria-disabled, and so do disabled days: Enter on one announces why it can't be picked.
  • While a range start is pending, its length limits are announced with it.
  • Focus returns to the input when the popup closes.
  • Selected, today and focus states stay visible in Windows high-contrast (forced-colors), and animations respect prefers-reduced-motion.

Keyboard Navigation

| Key | Where | Action | |---|---|---| | ↓ / Alt+↓ | Input | Open the calendar and focus the selected (or today's) date | | Enter | Input (allow-input) | Read the typed date | | ← → | Days | Previous / next day (mirrored in RTL locales) | | ↑ ↓ | Days | Same day in the previous / next week | | Home / End | Days | First / last day of the week (first-day-of-week, else the locale's) | | PageUp / PageDown | Days | Previous / next month | | Shift+PageUp / Shift+PageDown | Days | Previous / next year | | Enter / Space | Days, months, years | Select | | Arrow keys | Month / year view | Move between cells | | Escape | Anywhere in the popup | Close (month/year view: back to days) and return focus to the input | | Tab | Popup | Cycle through the controls inside the popup |

Keyboard focus never moves outside min/max.

Frameworks

React

import IntlDatepicker from 'intl-datepicker/react';

function App() {
  const ref = useRef(null);
  const [value, setValue] = useState('2026-03-15');

  return (
    <IntlDatepicker
      ref={ref}
      calendar="persian"
      locale="fa-IR"
      value={value}
      onChange={(detail) => setValue(detail.value)}
      onSelect={(detail) => console.log(detail)}
      onNavigate={(detail) => console.log(detail)}
      onOpen={(e) => { /* return false to prevent */ }}
      onClose={(e) => { /* return false to prevent */ }}
    />
  );
}

Works with React 17–19. Attributes are passed at render time, so the first paint already uses the right calendar and locale. The built file starts with 'use client', so it can be imported from Next.js App Router server components.

Ref API

ref.current.element;        // underlying HTMLElement
ref.current.value;          // value string
ref.current.displayValue;   // formatted string
ref.current.calendarValue;  // CalendarDate
ref.current.selectedDates;  // CalendarDate[]
ref.current.getValue();     // SelectDetail
ref.current.setValue('2026-04-05');
ref.current.clear();
ref.current.open();
ref.current.close();
ref.current.goToMonth(2026, 6);

Vue 3

Tell Vue the tag is a custom element (in vite.config.js: vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'intl-datepicker' } } })), then:

<script setup>
import 'intl-datepicker/full';
import { ref } from 'vue';
const date = ref('');
</script>

<template>
  <intl-datepicker calendar="persian" locale="fa-IR"
    :value="date" @intl-change="date = $event.detail.value" />
</template>

Svelte

<script>
  import 'intl-datepicker/full';
  let date = '';
</script>

<intl-datepicker calendar="hebrew" locale="he-IL"
  value={date} on:intl-change={(e) => (date = e.detail.value)} />

Angular

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'intl-datepicker/full';

@Component({
  selector: 'app-date',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<intl-datepicker calendar="islamic" locale="ar-SA"
    [value]="date" (intl-change)="date = $any($event).detail.value"></intl-datepicker>`,
})
export class DateComponent { date = ''; }

TypeScript

Type declarations are included. Imports:

import 'intl-datepicker';
import type {
  IntlDatepickerElement,
  SelectDetail,
  MonthDetail,
  NavigateDetail,
  DatepickerType,
  MapDaysFn,
  RangePreset,
  DisabledDatesFilterFn,
  DayInfo,
  IntlDatepickerLabels,
  PluralLabel,
} from 'intl-datepicker';

// React
import IntlDatepicker from 'intl-datepicker/react';
import type { IntlDatepickerProps, IntlDatepickerRef } from 'intl-datepicker/react';

Browser Support

Any browser supporting Web Components, form-associated custom elements and Intl.DateTimeFormat calendars:

  • Chrome/Edge 77+
  • Firefox 98+
  • Safari 16.4+

The top-layer popup uses the Popover API (Chrome 114, Firefox 125, Safari 17); older browsers fall back to a fixed-position popup.

License

MIT