Format Temporal dates with date-fns-style tokens. Just format() and parse(), nothing else.
This is the lite version of temporal-fmt — same two functions, same locales, ~52x smaller. It's meant to stay small. See why below.
No deps. Works natively on Node 26+. Bring a polyfill if you're older.
Locale stuff (month/weekday names) needs Node 20+ either way. Haven't tested below that.
npm install temporal-fmt-liteNode 26+: you're done, it's built in.
Older Node: grab a polyfill like temporal-polyfill:
import 'temporal-polyfill/global'
import { format, parse } from 'temporal-fmt-lite';Or set it manually if you don't want to touch globals (useful in libraries):
import { Temporal } from 'temporal-polyfill/full';
import { setTemporal, format, parse } from 'temporal-fmt-lite';
setTemporal(Temporal); // do this before calling format/parseCalling setTemporal() again just overwrites whatever was set before.
import { format } from 'temporal-fmt-lite';
const date = Temporal.PlainDate.from('2026-08-04');
format(date, 'yyyy-MM-dd'); // "2026-08-04"
format(date, 'MMMM d, yyyy'); // "August 4, 2026"
const dt = Temporal.PlainDateTime.from('2026-08-04T15:45:30');
format(dt, "MMM d, yyyy 'at' h:mm a"); // "Aug 4, 2026 at 3:45 PM"
const zdt = Temporal.ZonedDateTime.from('2026-08-04T15:45:30-04:00[America/New_York]');
format(zdt, 'yyyy-MM-dd HH:mm zzz'); // "2026-08-04 15:45 America/New_York"Want literal text in the output? Wrap it in single quotes, like 'at' above. Need an actual quote character? Use ''.
parse figures out whether you get a PlainDate, PlainTime, PlainDateTime, or ZonedDateTime based on which tokens you used:
import { parse } from 'temporal-fmt-lite';
parse('yyyy-MM-dd HH:mm', '2026-08-04 15:45'); // PlainDateTime
parse('yyyy-MM', '2026-08-04T15:45:30'); // throws, shape doesn't match
parse('yyyy-MM-dd', '2026-02-30'); // throws, Feb 30 isn't a dayYou'll need an instanceof check (or a type guard in TS) to know what you got back, since the return type depends on the format string.
It actually builds the date rather than just pattern-matching, so it'll also catch a weekday that doesn't match the date:
parse('EEEE, yyyy-MM-dd', 'Tuesday, 2026-08-04'); // fine, that's really a Tuesday
parse('EEEE, yyyy-MM-dd', 'Monday, 2026-08-04'); // throws, it's notSome uhh, random things:
yy(2-digit year) works like old-school strptime:00–68→2000–2068,69–99→1900–1999. Yes it's arbitrary, but it meansyydoesn't need some external reference date to resolve.- Mixing
hh/hwithHH/H, or usinghh/hwithout anatoken, throws. It won't guess which one you meant even if they'd agree on the hour anyway. Just pick one. MMMM/MMMname matching only really knows 12-month calendars. It's built off 12 Gregorian reference dates, so calendars with leap months (Hebrew, for instance) aren't fully covered by name. Numericyyyy-MM-ddis fine regardless.
Pass a BCP 47 tag as the third arg and month/weekday/AM-PM names localize. Default is 'en-US'.
format(date, 'MMMM d, yyyy', { locale: 'fr-FR' }); // "août 4, 2026"
format(date, 'EEEE d MMMM', { locale: 'ar-EG' }); // Arabic names
format(dt, 'h:mm a', { locale: 'ja-JP' }); // "3:45 午後"a matches AM/PM case-insensitively when parsing — pm, Pm, PM, whatever.
The name-based tokens (MMMM, MMM, EEEE, EEE, a) go through Intl.DateTimeFormat, so non-Gregorian calendars work too as long as your Temporal object already has one:
const hebrewDate = date.withCalendar('hebrew');
format(hebrewDate, 'MMMM d, yyyy'); // "Av 21, 5786"Same deal for parsing:
parse('MMMM d, yyyy','août 4, 2026', { locale: 'fr-FR' });
parse('h:mm a', '3:45 午後', { locale: 'ja-JP' });
parse('yyyy-MM-dd', '5786-11-21', { locale: 'en-u-ca-hebrew' }); // -u-ca- picks the calendarOne thing that's non-negotiable: numbers always come out as plain 0-9 digits, no matter the locale. Not a bug. Stuff that reads these values back — logs, APIs, filenames — wants boring ASCII digits, and the padding logic doesn't play nice with Arabic-Indic or Devanagari numerals anyway. If you want localized digits, run them through Intl.NumberFormat yourself.
| Token | Meaning | Example |
|---|---|---|
| yyyy | 4-digit year | 2026 |
| yy | 2-digit year | 26 |
| MMMM | full month name | August |
| MMM | short month name | Aug |
| MM | 2-digit month | 08 |
| M | month | 8 |
| dd | 2-digit day | 04 |
| d | day | 4 |
| EEEE | full weekday | Tuesday |
| EEE | short weekday | Tue |
| HH | 2-digit hour (24h) | 15 |
| H | hour (24h) | 15 |
| hh | 2-digit hour (12h) | 03 |
| h | hour (12h) | 3 |
| mm | 2-digit minute | 45 |
| m | minute | 45 |
| ss | 2-digit second | 30 |
| s | second | 30 |
| SSS | milliseconds | 000 |
| a | AM/PM | PM |
| zzz | IANA time zone id | America/New_York |
That's all of them, and there won't be more. Use a token your input doesn't support — HH on a plain date, say — and it throws a real error instead of quietly giving you undefined.
temporal-fmt started as just format() and parse(). Then it grew a CLI, IDE tooling, a plugin sandbox, business calendars, holiday calendars, recurrence rules, a timezone subsystem... all useful on their own, but together they turned a 200KB install into several megabytes for anyone who just wanted 'yyyy-MM-dd'. Numbers are in dirazcoder/temporal-fmt#9 if you want them.
This package is that original surface, pulled back out. Same core behavior, same feature set — just the classic format()/parse() combo, kept small on purpose.
What that means:
- No new tokens, functions, or options, ever. If it's not in this README it's not sneaking in later.
- This is small and done, not a growing product. If you outgrow it and need the CLI, recurrence rules, business calendars, whatever — go to
temporal-fmtitself. Since this package's API is a subset of that one's, migrating is just adding stuff, not rewriting.
- Numbers are always Western digits, see the locale section above.
- Locale tokens need Node 20+. Untested below that.
- You have to hook up Temporal yourself unless you're on Node 26+.
- On native Temporal (Node 26+), locale name tokens (
MMMM/MMM/EEEE/EEE) can get the wrong month or weekday for dates before ~1582 CE — that's an ICU calendar quirk, not something specific to this package. - Gluing two unpadded numeric tokens together with nothing between them (
Md,dM,Hm) is ambiguous sometimes, andparse()throws instead of guessing. Zero-pad (MM/dd) or add a separator and it's fine.
Apache-2.0