@mk01/react-hijri-date-picker
v2.0.1
Published
Accurate, accessible React Hijri (Islamic) date picker — Umm al-Qura via built-in Intl, zero dependencies.
Maintainers
Readme
🌙 React Hijri Date Picker
An accurate, accessible Hijri (Islamic) date picker for React — zero dependencies, no CSS import required.
Conversion is powered by the JavaScript engine's built-in Intl Islamic calendars, so dates match the official Umm al-Qura calendar used in Saudi Arabia (with civil and tabular variants also available), including real 29/30-day month lengths.
✨ Features
- ✅ Accurate conversion — Umm al-Qura, civil, or tabular Islamic calendars via built-in
Intl(zero dependencies) - ✅ Real month lengths — 29/30-day months, no invalid dates
- ✅
dayAdjustment— shift ±N days for local moon-sighting differences - ✅ Single, range, and multiple selection modes
- ✅ Full keyboard support — arrow keys, Home/End, PageUp/PageDown, Enter, Escape
- ✅ Screen-reader friendly — dialog/grid roles, roving tabindex, live month announcements
- ✅ RTL layout and Arabic-Indic numerals (١٤٤٦) out of the box for
locale="ar" - ✅ Localizable — override every string (month names, weekdays, buttons) via
labels - ✅ Numbered months — optional "1- Muharram" ordering hint via
numberedMonths - ✅ Gregorian day numbers inside cells + Gregorian equivalent footer
- ✅ Min/max dates, disabled dates,
shouldDisableDatecallback, highlighted dates - ✅ Manual typing with format-aware validation (accepts Arabic-Indic digits)
- ✅ Month/year zoom navigation (click the header)
- ✅ Week numbers, first-day-of-week, today indicator
- ✅ Inline mode (always-visible calendar) and portal rendering (escape
overflow: hidden) - ✅ Auto popup flipping (top/bottom based on viewport space)
- ✅ Light/dark themes, custom colors, and CSS-variable theming
- ✅ Controlled or uncontrolled (
value/defaultValue) - ✅ Form-ready:
name,id,required,error,autoFocus, imperative ref handle - ✅ SSR-safe, ships with
"use client"for Next.js App Router - ✅ Fully typed with TypeScript
📋 Requirements
- React 17 or newer (
reactandreact-domas peer dependencies) - Any modern browser, or Node.js 14+ for SSR (the Islamic calendars ship with the platform's
Intlimplementation — no polyfill needed)
📦 Installation
npm install @mk01/react-hijri-date-picker
# or
yarn add @mk01/react-hijri-date-pickerNo CSS import needed — styles are self-contained.
🚀 Quick start
import React, { useState } from 'react'
import HijriDatePicker from '@mk01/react-hijri-date-picker'
export default function App() {
const [date, setDate] = useState('1447-04-15')
return (
<HijriDatePicker
value={date}
onChange={(value, gregorianDate, hijri) => {
setDate(value)
console.log(value) // "1447-04-15"
console.log(gregorianDate) // JS Date (UTC midnight)
console.log(hijri) // { hy: 1447, hm: 4, hd: 15 }
}}
placeholder="Select date"
/>
)
}Leave value out (or use defaultValue) for uncontrolled usage.
🕌 Understanding the calendar variants
The Hijri calendar is lunar: religiously, a month starts when the new crescent is sighted. Software uses computed approximations, and the calendar prop picks which one:
| Variant | What it is | Use it for |
|---|---|---|
| umalqura (default) | The official astronomical calendar of Saudi Arabia, computed for the coordinates of Mecca | Anything user-facing, especially Saudi/Gulf audiences |
| civil | Arithmetic (tabular) calendar, Friday epoch — fixed 30/29-day pattern with an 11-leap-year cycle | Interop with systems/documents using the arithmetic calendar; historical dates |
| tabular | Same arithmetic calendar with the Thursday (astronomical) epoch — always one day ahead of civil | Interop with systems using that convention |
<HijriDatePicker
calendar="umalqura"
dayAdjustment={1} // local moon sighting one day ahead of the tables
onChange={setDate}
/>No computed calendar can predict actual moon-sighting announcements — committees occasionally declare Ramadan or Eid a day earlier or later than the tables. dayAdjustment shifts the whole calendar by ±N days for such regions: +1 means the local Hijri month started one day earlier than the tables say.
🌍 Arabic locale
RTL layout and Arabic-Indic numerals are automatic with locale="ar":
<HijriDatePicker
value={date}
onChange={setDate}
locale="ar"
format="D MMMM YYYY"
showGregorianEquivalent
/>Force digit style either way with numerals="latn" (1447) or numerals="arab" (١٤٤٧); override direction with dir="ltr" | "rtl".
📅 Selection modes
Each mode changes the shape of value and onChange:
// single (default)
<HijriDatePicker
value={date} // string
onChange={(value, gregorian, hijri) => {}}
// (value: string, gregorianDate?: Date, date?: HijriDate | null)
/>
// range — first click sets the start, second completes the range
<HijriDatePicker
mode="range"
value={range} // [string | null, string | null]
onChange={(value, gregorianDates) => {}}
// (value: [string|null, string|null], gregorianDates?: [Date|null, Date|null])
/>
// multiple — click days to toggle them
<HijriDatePicker
mode="multiple"
value={dates} // string[]
onChange={(values, gregorianDates) => {}}
// (value: string[], gregorianDates?: Date[])
/>🚫 Restrictions
<HijriDatePicker
minDate="1447-04-01"
maxDate="1447-04-30"
disabledDates={['1447-04-15']}
shouldDisableDate={(hijri, gregorian) => gregorian.getUTCDay() === 5} // no Fridays
highlightedDates={['1447-04-27']}
onChange={setDate}
/>⌨️ Manual input
<HijriDatePicker
allowManualInput
format="DD/MM/YYYY"
placeholder="Type or select: DD/MM/YYYY"
onChange={setDate}
/>Typing is single-mode only. Parsing follows the format prop, always accepts the ISO form YYYY-MM-DD, understands month names in English and Arabic, and accepts Arabic-Indic digits. Invalid text is discarded on blur.
🖼️ Inline calendar & portal popup
{/* Always-visible calendar, no input */}
<HijriDatePicker inline onChange={setDate} />
{/* Render the popup into document.body to escape overflow:hidden containers */}
<HijriDatePicker portal onChange={setDate} />
{/* Or into a specific element */}
<HijriDatePicker portal={myContainerElement} onChange={setDate} />🔢 Numbered months & month grid layout
<HijriDatePicker
numberedMonths // header and month grid show "1- Muharram", "2- Safar", …
monthsPerRow={2} // month-picker grid columns (1–4, default 2)
onChange={setDate}
/>With locale="ar" the numbering renders as "١- محرم".
🎨 Theming
Built-in themes plus per-color overrides:
<HijriDatePicker
theme="dark" // 'light' | 'dark' | 'custom'
customColors={{ primary: '#10b981', selected: '#059669' }}
/>customColors keys: primary, background, text, border, hover, selected, selectedText, disabled, range, error.
Or theme with CSS variables — no props needed, set them on any ancestor:
.my-app {
--hijri-dp-primary: #10b981;
--hijri-dp-background: #0f172a;
--hijri-dp-text: #f1f5f9;
--hijri-dp-border: #334155;
--hijri-dp-hover: #1e293b;
--hijri-dp-selected: #059669;
--hijri-dp-selected-text: #ffffff;
--hijri-dp-disabled: #64748b;
--hijri-dp-range: rgb(16 185 129 / 0.18);
--hijri-dp-error: #f87171;
--hijri-dp-focus-ring: rgb(16 185 129 / 0.3);
}CSS variables win over theme/customColors, so a design system can restyle every picker centrally.
🈯 Localization beyond en/ar
Every visible or screen-reader string can be replaced via labels:
<HijriDatePicker
labels={{
months: ['Muharrem', 'Safer', /* …12 names, Muharram first… */],
weekdays: ['Pz', 'Pt', 'Sa', 'Ça', 'Pe', 'Cu', 'Ct'], // short, Sunday first
weekdaysLong: ['Pazar', /* …7 full names for screen readers… */],
today: 'Bugün',
clear: 'Temizle',
gregorian: 'Miladi',
weekColumn: 'Hf',
previousLabel: 'Önceki',
nextLabel: 'Sonraki',
openCalendar: 'Hicri takvim',
chooseMonth: 'Ay seç',
chooseYear: 'Yıl seç'
}}
/>All keys are optional — anything you omit falls back to the locale defaults.
📝 Forms
<form onSubmit={handleSubmit}>
<HijriDatePicker
name="hijri_birth_date" // submitted with the form
id="birth-date"
required
error={showError} // red border + aria-invalid
aria-label="Birth date (Hijri)"
onChange={setDate}
/>
</form>🎛️ Imperative handle
import HijriDatePicker, { type HijriDatePickerHandle } from '@mk01/react-hijri-date-picker'
const pickerRef = useRef<HijriDatePickerHandle>(null)
<HijriDatePicker ref={pickerRef} onChange={setDate} />
pickerRef.current?.open() // open the popup
pickerRef.current?.close() // close it
pickerRef.current?.focus() // focus the input
pickerRef.current?.clear() // clear the selection
pickerRef.current?.input // the underlying HTMLInputElement🧰 Utility exports
The conversion engine is exported for standalone use — no component needed:
import {
gregorianToHijri, hijriToGregorian, daysInHijriMonth,
isValidHijriDate, todayHijri, addHijriDays,
compareHijriDates, isSameHijriDate, hijriWeekNumber,
formatHijriDate, parseHijriDate, formatNumber, normalizeDigits
} from '@mk01/react-hijri-date-picker'
gregorianToHijri(new Date(Date.UTC(2025, 2, 30))) // { hy: 1446, hm: 10, hd: 1 } — Eid al-Fitr
hijriToGregorian({ hy: 1447, hm: 1, hd: 1 }) // 2025-06-26 (UTC midnight)
daysInHijriMonth(1446, 9) // 29 — real Umm al-Qura month length
todayHijri() // today as { hy, hm, hd }| Function | Signature | Notes |
|---|---|---|
| gregorianToHijri | (date, calendar?, dayAdjustment?) → HijriDate | date is UTC midnight |
| hijriToGregorian | (date, calendar?, dayAdjustment?) → Date | Returns UTC midnight |
| daysInHijriMonth | (hy, hm, calendar?) → 29 \| 30 | |
| isValidHijriDate | (date, calendar?) → boolean | Rejects day 30 of a 29-day month |
| todayHijri | (calendar?, dayAdjustment?) → HijriDate | |
| addHijriDays | (date, days, calendar?) → HijriDate | Crosses month/year boundaries |
| compareHijriDates | (a, b) → number | <0, 0, >0 like a comparator |
| isSameHijriDate | (a, b) → boolean | Null-safe |
| hijriWeekNumber | (date, firstDayOfWeek?, calendar?) → number | Week 1 contains 1 Muharram |
| formatHijriDate | (date, format, months, numerals?) → string | |
| parseHijriDate | (input, format, monthLists) → HijriDate \| null | Accepts Arabic-Indic digits |
| formatNumber | (value, numerals) → string | 1447 → ١٤٤٧ |
| normalizeDigits | (text) → string | Arabic-Indic/Persian digits → ASCII |
All Gregorian Date values are UTC midnight of the calendar day — use getUTCDate() / toLocaleDateString(..., { timeZone: 'UTC' }) when reading them.
🧾 Props reference
Value & selection
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | 'single' \| 'range' \| 'multiple' | 'single' | Selection mode; changes value/onChange shapes (see Selection modes) |
| value | string / [string\|null, string\|null] / string[] | — | Controlled value (shape follows mode) |
| defaultValue | same as value | — | Uncontrolled initial value |
| onChange | per mode | — | Formatted value(s), Gregorian Date(s), and (single mode) the HijriDate |
| format | 'YYYY-MM-DD' \| 'D MMMM YYYY' \| 'DD/MM/YYYY' \| 'MM/DD/YYYY' | 'YYYY-MM-DD' | Display and emit format |
Calendar & language
| Prop | Type | Default | Description |
|---|---|---|---|
| calendar | 'umalqura' \| 'civil' \| 'tabular' | 'umalqura' | Islamic calendar variant |
| dayAdjustment | number | 0 | Shift ±N days for local moon sighting |
| locale | 'en' \| 'ar' | 'en' | Built-in language |
| labels | Partial<HijriDatePickerLabels> | — | Override any text (see Localization) |
| numberedMonths | boolean | false | Prefix month names with their number ("1- Muharram") |
| monthsPerRow | 1 \| 2 \| 3 \| 4 | 2 | Columns in the month-picker grid |
| numerals | 'latn' \| 'arab' | 'arab' for ar | Digit system for display |
| dir | 'ltr' \| 'rtl' \| 'auto' | 'auto' | Text direction (auto follows locale) |
| firstDayOfWeek | 0–6 | 0 (Sunday) | Weekday headers rotate to match |
| optionsStartYear / optionsEndYear | number | today −100 / +20 | Inclusive navigable year range |
Restrictions
| Prop | Type | Description |
|---|---|---|
| minDate / maxDate | string | Inclusive bounds (any accepted format) |
| disabledDates | string[] | Unselectable dates |
| shouldDisableDate | (hijri: HijriDate, gregorian: Date) => boolean | Dynamic disabling |
| highlightedDates | string[] | Outlined dates |
Display & behavior
| Prop | Type | Default | Description |
|---|---|---|---|
| inline | boolean | false | Permanent calendar, no input |
| portal | boolean \| HTMLElement | false | Render popup into document.body (or an element) |
| popupPosition | 'bottom' \| 'top' \| 'auto' | 'auto' | auto flips based on viewport space |
| showGregorianDates | boolean | false | Gregorian day number inside each cell |
| showGregorianEquivalent | boolean | false | Gregorian date of the selection under the grid |
| showWeekNumbers | boolean | false | Hijri week-of-year column |
| showTodayButton | boolean | true | "Today" footer button |
| showTodayIndicator | boolean | true | Outline today's cell |
| clearable | boolean | true | "Clear" footer button |
| allowManualInput | boolean | false | Typing (single mode only) |
| closeOnSelect | boolean | true | Close after selecting (never auto-closes in multiple mode) |
| customDayRenderer | (day: number, date: HijriDate) => ReactNode | — | Custom cell content |
| onOpen / onClose | () => void | — | Popup lifecycle |
| onMonthChange | (hy: number, hm: number) => void | — | Visible month changed |
Form, state & styling
| Prop | Type | Description |
|---|---|---|
| name / id | string | Applied to the input |
| required / autoFocus | boolean | Applied to the input |
| error | boolean | Red border + aria-invalid |
| disabled | boolean | Fully inert |
| readOnly | boolean | Shows the value; can't open or edit |
| placeholder | string | Localized default provided |
| aria-label | string | Input label for screen readers |
| theme | 'light' \| 'dark' \| 'custom' | Base palette |
| customColors | HijriDatePickerColors | Per-color overrides (see Theming) |
| className / containerClassName / inputClassName / popupClassName | string | Hook points for your own CSS |
⌨️ Keyboard & accessibility
| Key | Action |
|---|---|
| Enter / Space / ↓ on input | Open calendar |
| Arrow keys | Move focus by day (respects RTL) / week |
| Home / End | Start / end of week |
| PageUp / PageDown | Previous / next month (Shift for year) |
| Enter / Space on a day | Select |
| Escape | Close and return focus to input |
The popup is a non-modal dialog with a grid of days, roving tabindex, aria-selected/aria-disabled/aria-current="date" states, full-date aria-labels on every cell, and a polite live region announcing month changes.
🏷️ TypeScript
Everything is typed. Exported types:
import type {
HijriDatePickerProps, // union of the three mode prop shapes
HijriDatePickerSingleProps,
HijriDatePickerRangeProps,
HijriDatePickerMultipleProps,
HijriDatePickerHandle, // imperative ref
HijriDatePickerLabels,
HijriDatePickerColors,
HijriDate, // { hy, hm, hd }
HijriRangeValue, // [string | null, string | null]
CalendarVariant, // 'umalqura' | 'civil' | 'tabular'
DateFormat,
NumeralSystem // 'latn' | 'arab'
} from '@mk01/react-hijri-date-picker'❓ Notes & FAQ
- Why does the Hijri date differ from another app by a day? Different calendar variant or local moon sighting. Match the other system's variant via
calendar, or shift withdayAdjustment. - Server-side rendering works out of the box (Next.js, Remix, …); the bundle ships with
"use client"for the App Router. - Gregorian
Datevalues are UTC midnight — read them with UTC accessors to avoid timezone drift. - Day 30 of a 29-day month is invalid and rejected, including in controlled
valuestrings.
🔄 Migrating from v1
- Dates are now accurate. v1 used a mean-month approximation that drifted 1–3 days from the real calendar; stored v1 values may map to a neighboring day in v2.
- Months now have 29 or 30 days (v1 always showed 30). Invalid dates like day 30 of a 29-day month are rejected.
- The month/year
<select>dropdowns were replaced by header zoom navigation (click the month title). optionsEndYearis now inclusive and both default relative to the current year instead of 1400–1500.- The
./dist/index.cssexport was removed (it never shipped a file); no CSS import is needed. - The input no longer has a default
name="hijri-date"— passnameexplicitly. onChange(single mode) now receives a third argument: theHijriDateobject ornull.theme,customColors, and all v1 props otherwise work unchanged.
📄 License
MIT © Mohamed Khaled
