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

moment-less

v0.2.1

Published

Zero-dependency token-based formatting for the native JavaScript Temporal API

Readme

moment-less

Moment.js muscle memory, Temporal-native engine.

Format the native JavaScript Temporal API with the YYYY-MM-DD tokens you already know — zero dependencies, ~2.2 KB gzipped, no legacy Date anywhere in sight.

npm version bundle size CI license

Documentation · API Reference · Migrating from Moment.js


import { format, fromNow, calendar } from 'moment-less';

format(Temporal.Now.plainDateISO(), 'MMMM Do, YYYY');        // "April 9th, 2026"
format(Temporal.Now.plainDateTimeISO(), 'ddd h:mm A');       // "Thu 2:05 PM"

fromNow(Temporal.Now.instant().subtract({ hours: 3 }));      // "3 hours ago"
calendar(Temporal.Now.plainDateTimeISO());                   // "Today at 2:05 PM"

The token syntax you have in muscle memory, wired directly to Temporal's native, immutable, time-zone-aware primitives. No parsing engine, no mutation, no Date.

What is moment-less?

The TC39 Temporal API finally fixes JavaScript's broken Date object — it gives you immutable, time-zone-aware, calendar-correct date primitives. But Temporal deliberately ships without a formatting method: the spec delegates display to Intl.DateTimeFormat, which is powerful but painfully verbose for everyday YYYY-MM-DD HH:mm UI work.

Meanwhile Moment.js is officially in maintenance mode and recommends migrating to native platform APIs — but everyone migrating still wants to write format(date, 'MMM D, YYYY') without pulling in a 72 KB library that doesn't even understand Temporal objects.

moment-less is the missing bridge. It is intentionally scoped to one job — turn a Temporal object into a string — and leaves date math, parsing, and time-zone conversion to Temporal, which already does them better than any library can.

   Moment.js token syntax  ──►  moment-less  ──►  native Temporal fields
   (the part you keep)                            (the part Temporal already nails)

Why moment-less?

  • 🔤 Token syntax you already knowYYYY, MMMM, Do, dddd, HH:mm, A/a, and more. If you know Moment.js or day.js, there is nothing new to learn.
  • 📅 Every Temporal typePlainDate, PlainTime, PlainDateTime, ZonedDateTime, and Instant. Each type exposes exactly the tokens that make sense for it, with a descriptive error if you ask for one it can't provide.
  • 🕐 More than formatting — relative time (fromNow), calendar labels (calendar), duration humanizing (humanizeDuration), and a legacy-Date bridge (fromDate).
  • 🌍 Locale-aware — month names, weekday names, and relative phrases adapt to any BCP 47 locale through the built-in Intl APIs. No locale bundles to ship.
  • 📦 Zero production dependencies — nothing to audit, nothing to update. ~2.2 KB gzipped for the whole library; ~1.3 KB if you only import format.
  • 🧩 Tree-shakeable & typed — every function is an independent named export, with strict bundled TypeScript types (no @types/* package).
  • 🌐 Universal runtime — browser, Node.js, Deno, Bun, Cloudflare Workers, Vercel Edge.

Install

npm install moment-less
# or
pnpm add moment-less
# or
yarn add moment-less

Requires the Temporal API. It ships natively in Node 22+, Chrome 127+, Firefox 139+, Safari 18.2+, Deno 2.1+, and Bun 1.2+. For older targets, drop in a polyfill — see Runtime & browser support.

Quick start

import { format, fromNow, calendar, humanizeDuration, fromDate } from 'moment-less';

// 1. Token-based formatting — works on any Temporal type
const dt = Temporal.PlainDateTime.from('2026-04-09T14:05:30');
format(dt, 'YYYY-MM-DD');                 // "2026-04-09"
format(dt, 'dddd, MMMM Do [at] h:mm A');  // "Thursday, April 9th at 2:05 PM"

// 2. Relative time — no manual duration math
fromNow(dt.subtract({ hours: 3 }), dt);   // "3 hours ago"

// 3. Calendar labels — like chat apps and file managers
calendar(dt, Temporal.PlainDate.from('2026-04-09')); // "Today at 2:05 PM"

// 4. Humanize a duration
humanizeDuration(Temporal.Duration.from({ hours: 2, minutes: 45 })); // "3 hours"

// 5. Bridge a legacy Date from an API or DB driver
format(fromDate(new Date()), 'YYYY-MM-DD HH:mm');

Core concept: types decide tokens

A Temporal object only carries the fields its type makes sense for — a PlainDate has no clock, a PlainTime has no calendar. moment-less honours that: using a token a type can't supply throws a descriptive error instead of inventing data.

| Token group | PlainDate | PlainTime | PlainDateTime | ZonedDateTime | Instant¹ | |-------------|:-----------:|:-----------:|:---------------:|:---------------:|:----------:| | Year / month / day (YYYY MMMM Do …) | ✅ | ❌ | ✅ | ✅ | ✅ | | Weekday (dddd ddd) | ✅ | ❌ | ✅ | ✅ | ✅ | | Time (HH h mm ss SSS A/a) | ❌ | ✅ | ✅ | ✅ | ✅ | | UTC offset (Z ZZ) | ❌ | ❌ | ❌ | ✅ | ✅ | | Unix timestamp (X x) | ❌ | ❌ | ❌ | ✅ | ✅ |

¹ An Instant has no time zone of its own, so moment-less normalizes it to a UTC ZonedDateTime before formatting. Every field is then available and the offset is always +00:00.

format(Temporal.PlainDate.from('2026-04-09'), 'HH:mm');
// ❌ Error: Token "HH" requires an hour field
//    (not available on PlainDate — use PlainDateTime or ZonedDateTime)

API reference

All functions are pure, side-effect-free, and independently importable.


format(temporalObj, formatString, options?)

Converts any Temporal object into a string using Moment.js-style tokens.

| Parameter | Type | Description | |-----------|------|-------------| | temporalObj | PlainDate \| PlainTime \| PlainDateTime \| ZonedDateTime \| Instant | The value to format. | | formatString | string | A pattern of tokens and literal text. | | options.locale | string (optional) | BCP 47 tag for localized month/weekday names (e.g. 'fr', 'ja'). |

import { format } from 'moment-less';

const d = Temporal.PlainDate.from('2026-04-09');
format(d, 'YYYY-MM-DD');            // "2026-04-09"
format(d, 'MMMM Do, YYYY');         // "April 9th, 2026"
format(d, 'ddd, MMM D');            // "Thu, Apr 9"

const dt = Temporal.PlainDateTime.from('2026-04-09T14:05:59.123');
format(dt, 'hh:mm A');              // "02:05 PM"
format(dt, 'HH:mm:ss.SSS');         // "14:05:59.123"

const zdt = Temporal.ZonedDateTime.from('2026-04-09T14:05[Asia/Tokyo]');
format(zdt, 'YYYY-MM-DDTHH:mmZ');   // "2026-04-09T14:05+09:00"
format(Temporal.Now.instant(), 'X'); // "1775736000"  (Unix seconds, UTC)

// Localized names
format(d, 'dddd D MMMM', { locale: 'fr' }); // "jeudi 9 avril"

Escaping literal text. Wrap any text in [square brackets] to emit it verbatim — essential when literal words contain token letters:

format(dt, '[Today is] dddd');         // "Today is Thursday"
format(dt, 'h [o\'clock]');            // "2 o'clock"
format(dt, 'YYYY-MM-DD[T]HH:mm[Z]');   // "2026-04-09T14:05Z"  (literal T and Z)

fromNow(temporalObj, reference?, locale?)

Returns a human-readable relative-time string, powered by the native Intl.RelativeTimeFormat — no manual thresholds, fully localized.

| Parameter | Type | Description | |-----------|------|-------------| | temporalObj | PlainDate \| PlainDateTime \| ZonedDateTime \| Instant | The target time. PlainTime is unsupported (no date context). | | reference | Temporal.Instant (optional) | The "now" to measure against. Defaults to Temporal.Now. Pass an explicit value for deterministic tests. | | locale | string (optional) | BCP 47 tag for the output phrase. |

import { fromNow } from 'moment-less';

fromNow(Temporal.Now.instant().subtract({ hours: 3 }));   // "3 hours ago"
fromNow(Temporal.Now.plainDateISO().add({ days: 1 }));    // "tomorrow"
fromNow(Temporal.Now.instant().subtract({ years: 1 }));   // "last year"

// Deterministic: pass your own reference instant
fromNow(target, someFixedInstant);                        // "in 2 days"

// Localized
fromNow(target, undefined, 'fr');                         // "il y a 3 heures"
fromNow(target, undefined, 'es');                         // "hace 3 horas"

Date-aware by design. When you pass a PlainDate, moment-less compares calendar days, not raw instants — so fromNow(today) is reliably "today" and fromNow(tomorrow) is "tomorrow", regardless of the time of day or time zone.


calendar(temporalObj, reference?, options?)

Returns a context-aware calendar label in the style of Moment's .calendar() — the kind messaging apps and file managers use.

| Parameter | Type | Description | |-----------|------|-------------| | temporalObj | PlainDate \| PlainDateTime \| ZonedDateTime \| Instant | The date to label. | | reference | Temporal.PlainDate (optional) | The "today" to compare against. Defaults to Temporal.Now. | | options.locale | string (optional) | Locale for weekday/month names and the time portion. | | options.timeFormat | string (optional) | Token string for the time suffix. Default 'h:mm A'. | | options.labels | { today?, yesterday?, tomorrow?, at? } (optional) | Override the day words and the at connector (for i18n or custom copy). |

| Distance from reference | Output | |-------------------------|--------| | Same day | Today at 2:05 PM | | 1 day before / after | Yesterday at 11:30 AM / Tomorrow at 9:00 AM | | Within the surrounding week | Monday at 2:05 PM | | Further away | Apr 9, 2026 |

import { calendar } from 'moment-less';

calendar(Temporal.Now.plainDateTimeISO());               // "Today at 2:05 PM"
calendar(yesterdayDateTime);                             // "Yesterday at 11:30 AM"
calendar(oldDate);                                       // "Nov 15, 2025"

// Localized labels + 24-hour time + localized connector
calendar(pastDateTime, undefined, {
  locale: 'fr',
  timeFormat: 'HH:mm',
  labels: { today: "Aujourd'hui", yesterday: 'Hier', at: 'à' },
}); // "Hier à 14:05"

A PlainDate input produces a date-only label (no at … suffix).


humanizeDuration(duration, locale?)

Converts a Temporal.Duration into the most significant single-unit phrase — magnitude only, no "ago"/"in" direction.

| Parameter | Type | Description | |-----------|------|-------------| | duration | Temporal.Duration | The duration to describe. Sign is ignored (absolute magnitude). | | locale | string (optional) | BCP 47 tag for the output phrase. |

import { humanizeDuration } from 'moment-less';

humanizeDuration(Temporal.Duration.from({ hours: 2, minutes: 45 })); // "3 hours"
humanizeDuration(Temporal.Duration.from({ seconds: 45 }));           // "45 seconds"
humanizeDuration(Temporal.Duration.from({ days: 400 }));             // "1 year"

humanizeDuration(Temporal.Duration.from({ seconds: 45 }), 'fr');     // "45 secondes"

Months and years are calendar-approximate (30 and 365 days respectively); sub-second components down to nanoseconds are counted toward the total.


fromDate(date)

Bridges a legacy JavaScript Date (from an API, DB driver, or third-party lib) to a Temporal.Instant, ready to hand to format or fromNow.

| Parameter | Type | Description | |-----------|------|-------------| | date | Date | A legacy Date object. | | returns | Temporal.Instant | The equivalent instant (millisecond precision). |

import { fromDate, format } from 'moment-less';

const instant = fromDate(new Date('2026-04-09T14:05:00Z'));
format(instant, 'YYYY-MM-DD HH:mm:ss'); // "2026-04-09 14:05:00"

Token reference

| Token | Description | Example | |-------|-------------|---------| | YYYY | 4-digit year | 2026 | | YY | 2-digit year, zero-padded | 26 | | MMMM | Full month name | April | | MMM | Short month name | Apr | | MM | Month, zero-padded | 04 | | M | Month, no padding | 4 | | Do | Day of month, ordinal | 9th | | DD | Day of month, zero-padded | 09 | | D | Day of month, no padding | 9 | | dddd | Full weekday name | Thursday | | ddd | Short weekday name | Thu | | HH | Hour (24h), zero-padded | 14 | | H | Hour (24h), no padding | 14 | | hh | Hour (12h), zero-padded | 02 | | h | Hour (12h), no padding | 2 | | mm | Minute, zero-padded | 05 | | ss | Second, zero-padded | 59 | | SSS | Millisecond, zero-padded | 007 | | A | AM / PM (uppercase) | PM | | a | am / pm (lowercase) | pm | | Z | UTC offset, with colon | +09:00 | | ZZ | UTC offset, no colon | +0900 | | X | Unix timestamp (seconds) | 1775736000 | | x | Unix timestamp (milliseconds) | 1775736000000 | | […] | Escaped literal text | [at]at |

Tokens are matched longest-first (MMMM before MMM before MM before M, Do before DD before D), so there is no ambiguity. Any unbracketed letters that form a token are interpreted as one — wrap literal words like Day, or a trailing Z, in […]. See the type matrix for which tokens each Temporal type supports.

Locale support

format localizes month and weekday names via options.locale; fromNow, calendar, and humanizeDuration localize their phrases via their locale argument. Everything flows through the platform's Intl — there are no locale data files to import or ship.

format(date, 'dddd D MMMM', { locale: 'ja' });  // "木曜日 9 4月"
fromNow(past, undefined, 'es');                  // "hace 3 horas"
humanizeDuration(dur, 'de');                     // "2 Stunden"

Numeric tokens (Do ordinal suffix, A/a) currently render in English regardless of locale.

Migrating from Moment.js

Moment.js is in maintenance mode and recommends migrating to native platform APIs. With Temporal handling the data and moment-less handling the display, the move is nearly token-transparent:

// Before — Moment.js (~72 KB gzipped, mutable, legacy Date)
import moment from 'moment';
moment().format('YYYY-MM-DD');
moment().add(1, 'days').format('DD/MM/YYYY');
moment(someDate).fromNow();

// After — moment-less + Temporal (~2.2 KB gzipped, immutable, no legacy Date)
import { format, fromNow } from 'moment-less';
format(Temporal.Now.plainDateISO(), 'YYYY-MM-DD');
format(Temporal.Now.plainDateISO().add({ days: 1 }), 'DD/MM/YYYY');
fromNow(Temporal.Now.instant());

What moves to Temporal (and why that's better):

  • ParsingTemporal.PlainDate.from('2026-04-09') instead of moment('2026-04-09') — strict, unambiguous, no silent fallbacks.
  • Manipulation → Temporal's built-in .add() / .subtract() / .until() — immutable, returns new objects.
  • Time zonesTemporal.ZonedDateTime directly — first-class, DST-correct, no plugin.

moment-less vs moment.js, date-fns, and day.js

| Library | Zero deps | Works with Temporal | Gzipped | Token syntax | |---------|:---------:|:-------------------:|--------:|:------------:| | moment-less | ✅ | ✅ native | ~2.2 KB | ✅ | | moment | ❌ | ❌ legacy Date | ~72 KB | ✅ | | date-fns | ❌ | ❌ legacy Date | ~13 KB (tree-shaken) | ✅ | | day.js | ❌ | ❌ legacy Date | ~2.9 KB | ✅ | | Intl.DateTimeFormat | ✅ | ✅ via Instant | 0 B | ❌ verbose |

moment-less is intentionally scoped: format Temporal objects, nothing else. Date arithmetic, parsing, and time-zone conversion are Temporal's job.

Runtime & browser support

moment-less is a pure formatting utility — no DOM, no Node built-ins. It runs anywhere Temporal is available, natively or polyfilled.

Native Temporal (no polyfill):

| Runtime | Minimum version | |---------|-----------------| | Node.js | 22.0+ | | Chrome / Edge | 127+ | | Firefox | 139+ | | Safari | 18.2+ | | Deno | 2.1+ | | Bun | 1.2+ |

Older environments — install a polyfill and import it once at your entry point:

npm install temporal-polyfill
import 'temporal-polyfill/global'; // installs Temporal on globalThis
import { format } from 'moment-less';

format(Temporal.Now.plainDateISO(), 'YYYY-MM-DD');

moment-less has no opinion on where Temporal comes from — native or polyfilled, it behaves identically. It also works out of the box on V8-based edge runtimes (Cloudflare Workers, Vercel Edge).

Bundle size

| Import | Gzipped | |--------|--------:| | Full library (all five functions) | ~2.2 KB | | format + fromNow | ~1.7 KB | | format only (tree-shaken) | ~1.3 KB |

Zero production dependencies.

FAQ

How do I format a date as YYYY-MM-DD with Temporal? format(Temporal.Now.plainDateISO(), 'YYYY-MM-DD'). No setup beyond the import.

Is this a drop-in Moment.js replacement? For formatting and relative time, yes — the token syntax is intentionally compatible. For parsing and date math, use Temporal directly (.from(), .add(), .subtract()); Temporal replaces those parts of Moment natively.

Does it polyfill Temporal? No. moment-less is formatting only. Use temporal-polyfill or @js-temporal/polyfill where Temporal isn't native yet.

Why not just use Intl.DateTimeFormat? It's great for locale-sensitive display but unwieldy for structural formats like YYYY-MM-DD HH:mm:ss — you'd call .formatToParts(), reduce the array, and zero-pad by hand. moment-less makes that a one-liner.

Does it support TypeScript? Fully. Types are bundled — no separate @types/moment-less. Every Temporal type is inferred correctly.

Is it tree-shakeable? Yes. Each function is an independent named export; importing only format ships ~1.3 KB gzipped.

Documentation

Full guides, the complete API reference, and framework recipes (React, Vue) live at thinkgrid-labs.github.io/moment-less.

Contributing

Issues and pull requests are welcome.

pnpm install
pnpm test            # run the suite
pnpm test:coverage   # with coverage
pnpm typecheck       # strict type check

License

MIT © thinkgrid-labs