Display Islamic (Hijri) Dates in JavaScript
To display a Hijri (Islamic) date, format an instant with Intl.DateTimeFormat using calendar: 'islamic' (or the islamic-umalqura variant), or attach the calendar to a Temporal value with .withCalendar('islamic-umalqura') and read .year, .month, and .day. This page is part of Calendar Systems and Era Handling.
Why This Scenario Is Tricky
Displaying a Hijri (Islamic) date is harder than swapping a calendar name because the Islamic calendar is lunar, and there is genuinely more than one variant of it in common use. Some variants are tabular β computed from an arithmetic rule β while others, notably Umm al-Qura used officially in Saudi Arabia, follow astronomical sighting rules, and these variants can differ from each other by a day for the same Gregorian date. That means "the Hijri date" is not a single well-defined value; you must choose the variant your audience expects, and choosing wrong produces dates that are visibly off by a day to users who know their own calendar.
The lunar nature also means months are 29 or 30 days with no simple pattern, and the civil day that a Gregorian instant maps to depends on the time zone, because the date can roll over at a different moment. Temporal and Intl expose the variants through the locale calendar extension (-u-ca-islamic-umalqura, -u-ca-islamic-tbla, and others), so the platform's data does the conversion, but you remain responsible for picking the right variant and pinning the zone so the displayed civil day is deterministic.
There is no single "Islamic calendar." The Hijri calendar is lunar, and the start of each month historically depends on the sighting of the new moon β something that can legitimately differ by a day between observers and computation methods. CLDR therefore ships several variants, and they routinely disagree by one day:
islamic-umalquraβ the Umm al-Qura calendar used officially in Saudi Arabia; the most common choice for civil applications.islamic(a.k.a.islamic-civil/islamicc) β a tabular, arithmetic calendar with fixed month-length rules.islamic-tblaβ tabular with an astronomical (Thursday) epoch.islamic-rgsaβ Saudi sighting-based.
Pick the wrong variant and a date can render as, say, Ramadan 14 instead of Ramadan 15. Legacy Date offers nothing here β it is Gregorian-only with no calendar option β so the conversion has to come from Intl or Temporal, both of which read CLDR data.
The matrix below shows how the same Gregorian instant maps to slightly different Hijri days depending on the chosen variant.
Minimal Working Solution
The fastest way to display a Hijri date is Intl.DateTimeFormat with a calendar-tagged locale:
// '-u-ca-islamic-umalqura' selects the Umm al-Qura variant via the locale extension
const fmt = new Intl.DateTimeFormat('en-US-u-ca-islamic-umalqura', {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC', // pin the zone so the displayed civil day is deterministic
});
console.log(fmt.format(new Date('2024-03-11T00:00:00Z'))); // 'Ramadan 1, 1445 AH'
To get the numeric Hijri fields (not just a display string), convert with Temporal:
import { Temporal } from '@js-temporal/polyfill';
const hijri = Temporal.PlainDate.from('2024-03-11').withCalendar('islamic-umalqura');
console.log(hijri.year); // 1445
console.log(hijri.month); // 9 (Ramadan)
console.log(hijri.day); // 1
Full Production Version
For display, Intl.DateTimeFormat with an Islamic calendar extension on the locale is the workhorse: it renders the day, month name, and year in the chosen variant and script, and it knows the month names so you never maintain a table. Pinning timeZone in the formatter options is essential, because without it the formatter uses the runtime's zone and the same instant can show as two different Hijri days depending on where the code runs β exactly the kind of nondeterminism that produces "the date is wrong on the server but right on my laptop" bug reports. Fixing the zone (often to UTC for storage-facing displays, or to the user's zone for user-facing ones) makes the output reproducible.
When you need to compute rather than just format β arithmetic on Hijri dates, or reading the Hijri month and year as numbers β Temporal.PlainDate.withCalendar('islamic-umalqura') (or the appropriate variant id) gives you a calendar-aware date whose month, day, and year are the Hijri components, while the ISO fields remain available. Store dates in the ISO/Gregorian form as the canonical value and derive the Hijri presentation on demand, so a single unambiguous representation lives in your data and the variant choice is a display-time decision you can change without migrating anything.
A production converter should let the caller choose the variant, validate it against a known set, cache the formatter per locale+variant, and return both structured fields and a localized string.
import { Temporal } from '@js-temporal/polyfill';
type IslamicVariant =
| 'islamic-umalqura'
| 'islamic-civil'
| 'islamic-tbla'
| 'islamic-rgsa';
interface HijriDate {
year: number;
month: number; // 1 = Muharram β¦ 9 = Ramadan β¦ 12 = DhuΚ»l-Hijjah
day: number;
display: string;
}
// Cache one formatter per locale+variant key β constructing Intl formatters is costly
const formatterCache = new Map<string, Intl.DateTimeFormat>();
function getFormatter(locale: string, variant: IslamicVariant): Intl.DateTimeFormat {
const key = `${locale}|${variant}`;
let fmt = formatterCache.get(key);
if (!fmt) {
fmt = new Intl.DateTimeFormat(`${locale}-u-ca-${variant}`, {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC', // PlainDate has no zone; UTC keeps the civil day stable on servers
});
formatterCache.set(key, fmt);
}
return fmt;
}
export function toHijri(
input: string | Temporal.PlainDate,
variant: IslamicVariant = 'islamic-umalqura',
locale = 'en-US'
): HijriDate {
const iso =
typeof input === 'string' ? Temporal.PlainDate.from(input) : input;
// Note: Temporal exposes the variants under the same ids used by Intl
const hijri = iso.withCalendar(variant);
// Intl.format() needs a Date; build it at UTC midnight to avoid a day-shift west of UTC
const asDate = new Date(Date.UTC(iso.year, iso.month - 1, iso.day));
return {
year: hijri.year,
month: hijri.month,
day: hijri.day,
display: getFormatter(locale, variant).format(asDate),
};
}
console.log(toHijri('2024-03-11'));
// { year: 1445, month: 9, day: 1, display: 'Ramadan 1, 1445 AH' }
console.log(toHijri('2024-03-11', 'islamic-civil').display);
// 'Ramadan 2, 1445 AH' β one day later than Umm al-Qura for this instant
Verifying That Variants Differ by a Day
This assertion block proves the day-offset behaviour is real, so you choose a variant deliberately rather than discover the mismatch in production:
import { Temporal } from '@js-temporal/polyfill';
const iso = Temporal.PlainDate.from('2024-03-11');
const umalqura = iso.withCalendar('islamic-umalqura');
const civil = iso.withCalendar('islamic-civil');
// Same Gregorian day, same Hijri month/year hereβ¦
console.assert(umalqura.year === 1445, 'Umm al-Qura year is 1445');
console.assert(umalqura.month === 9, 'Umm al-Qura month is Ramadan (9)');
// β¦but the day-of-month can differ between variants
console.assert(
umalqura.day !== civil.day,
'Umm al-Qura and tabular civil disagree on the day for this instant'
);
console.assert(civil.day === umalqura.day + 1, 'Civil is one day ahead here');
Common Pitfalls
The dominant pitfall is treating "Islamic calendar" as one thing. Pick the specific variant your users expect β Umm al-Qura for Saudi official use, a tabular variant elsewhere β because they can disagree by a day, and a mismatch is immediately visible to anyone who reads Hijri dates. The second pitfall is omitting the time zone from the formatter, which lets the runtime zone decide which civil day the instant maps to and makes the display nondeterministic across environments. Always pin the zone.
A third mistake is hard-coding Hijri month names or trying to compute the calendar arithmetically yourself; the lunar month lengths follow no simple pattern and the sighting-based variants are not formula-derivable, so rely on the platform's CLDR/ICU data through Intl or withCalendar. A fourth is discarding the ISO date once you have the Hijri components β keep the ISO form as canonical for storage and comparison, and treat the Hijri version as a derived display. Finally, be aware that older or minimal JavaScript runtimes may ship incomplete ICU data and support fewer calendar variants, so verify the variant you need is available in your deployment target rather than assuming it.
- Assuming
'islamic'is a single fixed calendar. It resolves to a tabular variant that can differ from the official Umm al-Qura date by a day. Wrong:new Intl.DateTimeFormat('en-u-ca-islamic')for a Saudi civil app. Right: name the variant explicitly, e.g.'...-u-ca-islamic-umalqura'. - Reading Hijri fields off a Gregorian value.
iso.monthis the Gregorian month, not Ramadan. Wrong:Temporal.PlainDate.from('2024-03-11').month. Right:.withCalendar('islamic-umalqura').month. - Dropping the
timeZonewhen formatting. WithouttimeZone: 'UTC', a midnightDatecan render as the previous Hijri day in negative-offset zones. Wrong:new Intl.DateTimeFormat('en-u-ca-islamic-umalqura', { dateStyle: 'long' }). Right: addtimeZone: 'UTC'. - Rebuilding the formatter per row. Wrong:
dates.map(d => new Intl.DateTimeFormat('en-u-ca-islamic-umalqura', opts).format(d)). Right: cache one formatter per locale+variant and reuse it.
Frequently Asked Questions
Which Islamic calendar variant should I use?
For civil/official use, islamic-umalqura (Umm al-Qura) is the safest default and matches Saudi government dates. Use islamic-civil (tabular) only when you need a purely arithmetic, deterministic calendar with no sighting dependency. Whatever you choose, name it explicitly rather than relying on the bare 'islamic' id.
Why do two Hijri converters show dates one day apart?
Because the Hijri calendar's month start depends on the new moon, and the CLDR variants model that with different rules β some sighting-based, some tabular. A Β±1 day spread between islamic-umalqura, islamic-civil, and islamic-tbla for the same Gregorian instant is expected, not a bug. Pick one variant and use it consistently across storage and display.
How do I display a date in the Islamic (Hijri) calendar in JavaScript?
Use Intl.DateTimeFormat with an Islamic calendar extension on the locale, such as 'en-US-u-ca-islamic-umalqura', and pin timeZone in the options so the civil day is deterministic. The formatter renders the day, month name, and year in the chosen variant and script using the platform's calendar data. For numeric components or arithmetic, use Temporal.PlainDate.withCalendar('islamic-umalqura'), which exposes the Hijri month, day, and year.
Why do different Islamic calendar variants show different dates?
Because the Islamic calendar is lunar and comes in several variants: some are tabular (computed from an arithmetic rule) and others, like Umm al-Qura, follow astronomical sighting rules. For the same Gregorian date these can differ by a day. There is no single 'Hijri date', so choose the variant your audience expects β Umm al-Qura for Saudi official use, a tabular variant elsewhere β via the locale calendar extension.
Why must I set a time zone when formatting a Hijri date?
Because the Gregorian instant maps to a civil day that can roll over at a different moment depending on the zone, so without a pinned timeZone the formatter uses the runtime's zone and the same instant can display as two different Hijri days across environments. Pinning the zone β UTC for storage-facing output, the user's zone for user-facing output β makes the displayed date reproducible.