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

@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.

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.

npm version License: MIT React

▶ Live demo


✨ 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, shouldDisableDate callback, 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 (react and react-dom as peer dependencies)
  • Any modern browser, or Node.js 14+ for SSR (the Islamic calendars ship with the platform's Intl implementation — no polyfill needed)

📦 Installation

npm install @mk01/react-hijri-date-picker
# or
yarn add @mk01/react-hijri-date-picker

No 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 | 06 | 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 with dayAdjustment.
  • Server-side rendering works out of the box (Next.js, Remix, …); the bundle ships with "use client" for the App Router.
  • Gregorian Date values 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 value strings.

🔄 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).
  • optionsEndYear is now inclusive and both default relative to the current year instead of 1400–1500.
  • The ./dist/index.css export was removed (it never shipped a file); no CSS import is needed.
  • The input no longer has a default name="hijri-date" — pass name explicitly.
  • onChange (single mode) now receives a third argument: the HijriDate object or null.
  • theme, customColors, and all v1 props otherwise work unchanged.

📄 License

MIT © Mohamed Khaled