There’s no doubt Moment.js is one of the most popular libraries in the JavaScript ecosystem, but now that it’s considered a legacy project in maintenance mode and its use is discouraged, you may be looking for some alternatives.
At first, looking for an alternative library may not seem like an easy task because there’s a lot of things you can do with Moment.js. For example:
However, most projects don’t need all this functionality. While some projects may use Moment.js to format dates and times in a particular way (relative dates and times are popular), for other projects it may be more important to check if a date is before, after, or between other dates or displaying dates according to the locale of the user.
So probably, many projects can satisfy their requirements by using a combination of native JavaScript objects (such as Date and Intl) and optionally, one lightweight library for specific purposes.
Last year, I wrote an article reviewing alternatives to Moment.js in the context of internationalization. The libraries reviewed in that article (luxon, date-fns, day.js) are still good alternatives to Moment.js, but in this article, I’ll review the functionality of three more libraries:
Also, I’ll revisit the Intl.RelativeTimeFormat object, which reached Stage 4 at the beginning of 2020 and is now supported by more browsers.
Let’s get started.
If you have worked with Java, you won’t have a hard time learning how to use this library because it’s a port of the Date/Time API that was introduced in Java 8 (which in turn, is based on the library Joda-Time, hence the name).
js-joda is organized into a set of immutable core classes. The main ones are:
2020-12-01
10:00:01.999999999
2020-12-01T10:00:01.999999999
These classes don’t provide time zone information. For this, you have to use ZonedDateTime, which stores the date, time, and time zone information, and ZoneId or ZoneOffset to work with particular time zones (you’ll also need to import the package @js-joda/timezone
).
The library also provides other classes (such as ChronoField) and types to represent portions of a date or time (such as Month). In particular, there’s a set of classes that represent amounts and points in time:
1970-01-01T00:00:00Z
(including time zone information). For example, 2020-12-01T10:00:47.202Z
Most of these classes have a common interface so they can implement the same methods in different ways. For example, Temporal provides a contract with methods for manipulating date-time fields (such as days or seconds) that are implemented by classes such as Instant
, LocalTime
, and ZonedDateTime
, among others.
This way, for parsing or creating date-time objects, all the classes have variations of the methods of
, from
, parse
, and now
. Here are some examples:
// Creates a LocalDate object from a year, month, and dayOfMonth value const ld1 = LocalDate.of(2020, Month.DECEMBER, 1); // Creates a LocalTime object from an ISO 8601 string const lt1 = LocalTime.parse("10:01:00.123456789"); // Creates a LocalDateTime object from the current datetime in UTC time const ldt1 = LocalDateTime.now(ZoneOffset.UTC); // Creates a ZonedDateTime ojbect from from a local date, time, and a time zone const zdt1 = ZonedDateTime.of(ld1, lt1, ZoneId.of("Europe/Paris")); // Creates an instant from the ZonedDateTime object const i1 = Instant.from(zdt1); // Creates a Period object from a text string such as PnYnMnD const p1 = Period.parse("P1Y10M"); // Creates a Duration object from a number of standard hours (positive or negative) const d1 = Duration.ofHours(-48);
The with*
methods can be used as setters (creating a new instance since everything is immutable):
// Returns a copy of the LocalDate with the value 2020/12/3 const ld2 = ld1.withDayOfMonth(3); // Returns a copy of the Instant object with the specified seconds but without changing the nanoseconds part const i2 = i1.withFieldValue(ChronoField.INSTANT_SECONDS, 923232434);
Or you can use the at*
methods to combine two instances of different types:
// Combines a LocalDate and a LocalTime object to create a new LocalDateTime const ldt2 = ld1.atTime(lt1); // Combines a LocalDateTime object and a time zone to create a ZonedDateTime object const zdt2 = ldt1.atZone(ZoneId.of("+04:00"));
In a similar way, the methods to query and check for conditions start with is
:
console.log(ld1.isLeapYear()); console.log(ldt1.isBefore(ldt2));
And there are plus
and minus
method to manipulate the date/time information of the objects:
// Add 10 minutes: 10:11:00.123456789 const lt2 = lt1.plusMinutes(10); // Add a duration const ldt3 = ldt1.plus(d1); // Subtract one hour: 09:01:00.123456789 const lt3 = lt1.minus(1, ChronoUnit.HOURS); // Substract a period const zdt3 = zdt1.minusAmount(p1);
Regarding date formatting, js-joda doesn’t have as many options as moment.js, but it does have a set of pattern strings (the same patterns used in Java) for custom formats. Here are some examples:
// 10:00 console.log(lt2.format(DateTimeFormatter.ofPattern("h:m"))); // 2019, 1 console.log(zdt3.format(DateTimeFormatter.ofPattern("y, Q")));
However, if your pattern contains text, you’ll have to use a locale.
To do so, import the package @js-joda/timezone
along with @js-joda/locale
to import all locales, or an individual locale package (@js-joda/locale_de
, for example):
import "@js-joda/timezone"; import { Locale } from "@js-joda/locale_de"; // ... // Formatting text with the DE locate: Okt. 1 2:52 PM console.log( ldt3.format( DateTimeFormatter.ofPattern("MMM d h:m a").withLocale(Locale.GERMAN) ) );
You can try all the examples in this sandbox.
Sugar is a library that provides many utility functions to work with arrays, numbers, objects, and dates, among other types.
The library contains many modules and polyfills, with a total size of around 38k gzip. However, if you don’t plan to use the complete set of functionality, you have three options:
Sugar has three modes of use:
Sugar
global object, organized into namespaces that correspond to the modules that extend the native classes. The methods are called directly on the object. For example, Sugar.Date.create("2020-01-01")
new Sugar.Date("2020-01-01").isLeapYear().raw
Sugar.Date.extend(); console.log(new Date().isLeapYear());
Here I’ll use the default mode.
To create an instance use the create()
method passing a variety of formats, many of them unconventional:
const d1 = Sugar.Date.create("last month"); const d2 = Sugar.Date.create("in 2 hours"); const d3 = Sugar.Date.create("20th of May"); const d4 = Sugar.Date.create("13 Jan 2014 11:00:00 CST"); const d5 = Sugar.Date.create("the 2nd Friday of October 2009"); const d6 = Sugar.Date.create("6-2017"); // months are zero-based, this is actually May, 2017 const d7 = Sugar.Date.create("5 minutes ago");
You can see a lot of examples of how to create dates in the unit tests of the library.
Once you have created a Sugar instance, you can manipulate the date using the set
method, which also accepts a boolean parameter to reset the units more specific than those passed (just remember that months are zero-based):
// Sets month to January Sugar.Date.set(d1, { month: 0 }); // Sets day to 10 and reset hours to midnight Sugar.Date.set(d2, { day: 10 }, true);
In Sugar, not all methods are immutable. Here you can find the list of methods that mutate the date object.
There are also methods to shift the date forward and backwards (advance()
and rewind()
respectively), add single units of time add*()
methods) and moving the date to the beginning or end of a unit of time (beginningOf*()
and endOf*()
respectively):
// Shifts the date forward one month Sugar.Date.advance(d3, { months: 1 }); // Keys can be singular too: month // Shifts the date backwards 2 hours Sugar.Date.rewind(d4, { hours: 2 }); // Adds 1 month Sugar.Date.addMonths(d5, 1); // Sets the date to Jan 1st at midgnight of the same year Sugar.Date.beginningOfYear(d6); // Sets the time to 11:59:59 Sugar.Date.endOfDay(d7);
Methods beginning with is
allow us to compare or test dates:
// Is the year of d1 2020? Sugar.Date.is(d1, '2020'); // Is d1 before January 2nd, 2020? Sugar.Date.isBefore(d1, 'January 2nd, 2020'); // Is d1 after December 2018? Sugar.Date.isAfter(d1, 'December 2018'); // Is d1 Friday? Sugar.Date.isFriday(d1); // Is d1 a date in the future? Sugar.Date.isFuture(d1); // Is d1 a weekday? Sugar.Date.isWeekday(d1);
Sugar also provides a way to get time differences in many units with the methods *Since
, *Ago
, *Until
, and *FromNow
:
// How many months have passed since d4? Sugar.Date.monthsSince(d4); // How many years have passed between d4 and d5? Sugar.Date.yearsSince(d4, d5); // How many days ago was d4) Sugar.Date.daysAgo(d4); // How many hours aga from d5 was d4? Sugar.Date.hoursAgo(d5, d4); // How many weeks from d4 until now? Sugar.Date.weeksUntil(d4); // How many seconds until d5 from d4? Sugar.Date.secondsUntil(d5, d4); // How many ms from now to d4) Sugar.Date.millisecondsFromNow(d4);
But what I like most about Sugar is all the options it provides for formatting. The format
method supports two types of tokens, LDML, and strftime.
LDML, which are formats that are both short and easy to remember (search for a list of tokens here):
Sugar.Date.format(d3, "{Weekday}, {hours}:{mm}:{ss}{TT}"); // e.g. Saturday, 12:00:00AM
And strftime, which is used in other programming languages, such as Python (search for a list of tokens here):
Sugar.Date.format(d3, "{Weekday}, {hours}:{mm}:{ss}{TT}"); // e.g. Saturday, 12:00:00AM
In addition, there are four predefined format patterns (short
, medium
, long
, and full
):
Sugar.Date.short(d6); // 01/01/2017 Sugar.Date.medium(d6); // January 1, 2017 Sugar.Date.long(d6); // January 1, 2017 12:00 AM Sugar.Date.full(d6); // Sunday, January 1, 2017 12:00 AM
And two methods for relative time (relative
and relativeTo
) that automatically choose the most appropriate unit:
Sugar.Date.relative(d2); // 2 weeks ago Sugar.Date.relativeTo(d2, d5); // 10 years
English is the default locale included automatically. At the time of this writing, Sugar supports 17 locales, which are included in the official build or added separately via the downloads page.
A locale can be set globally with the setLocale
method, or passed as an argument to locale dependent methods such as isLastWeek
(so it can know the beginning of the week), create()
, or relative()
:
import "sugar-date/locales"; // Sets the locale to italian globaly // Sugar.Date.setLocale("it"); // Parse the string with the spanish locale const d8 = Sugar.Date.create("hace 5 dias", "es"); // Uses the default locale, english (or the one set with setLocale) // It doesn't use the locale used to create the instance Sugar.Date.full(d8); // Uses the french locale Sugar.Date.full(d8, "fr");
You can try all the examples in this sandbox.
Spacetime is a library to parse, manipulate, compare, and format dates with a special focus on time zones and daylight saving time (DST) to avoid errors when manipulating times in time zones with different DST rules.
You have to be aware of some considerations, however, spacetime has an API very similar to Moment.js (with some nice additions), with the difference that all its methods are immutable.
For example, you can create a Spacetime instance using many input formats and some helper methods:
/ ISO Format const s1 = spacetime("2020-12-01"); // As long date const s2 = spacetime("Dec 02 2020 17:50"); // As epoch in ms const s3 = spacetime(1606975200000); // As an array (months are zero-based) const s4 = spacetime([2020, 0, 1, 20, 0]); // As an object (months are zero-based) const s5 = spacetime({year:2020, month:0, date:1}); // Current time const s6 = spacetime.now(); // Today at midgnight const s7 = spacetime.today(); // Tomorrow at midnight const s8 = spacetime.tomorrow();
Getters and setters are handled in the same way that Moment.js is, with a few helpful additions such as season()
, hourFloat()
, and progress()
:
// Sets a new date based on s1 with 400 milliseconds const s9 = s1.millisecond(400); // Get milliseconds: 400 console.log("s9.milliseconds(): " + s9.millisecond()); // Get month (zero-based): 11 console.log("s9.month(): " + s9.month()); // Get day of year: 336 console.log("s9.dayOfYear(): " + s9.dayOfYear()); // Get day of year: winter console.log("s9.season(): " + s9.season()); // Set the hour + minute in decimal form const s10 = s2.hourFloat(16.5); // Get the time: 4:30pm console.log("s10.time(): " + s10.time()); // How far the moment lands between the start and end of the day/week/month/year (percentage-based) console.log("s3.progress('year'): " + s3.progress('year'));
The same happens with query or comparison methods:
// s3: 2020-12-03 // s4: 2020-01-01 console.log("s3.isAfter(s4): " + s3.isAfter(s4)); console.log("s3.isBefore(s4): " + s3.isBefore(s4)); console.log("s3.isEqual(s4): " + s3.isEqual(s4)); console.log("s3.leapYear(): " + s3.leapYear()); // Detect if two date/times are the same day, week, or year, etc console.log("s3.isSame(s4, 'year'): " + s3.isSame(s4, "year")); // Given a date amd a unit, count how many of them you'd need to make the dates equal console.log("s3.diff(s4, 'day'): " + s3.diff(s4, "day")); // Is daylight-savings-time activated right now, for this timezone? console.log("s3.inDST(): " + s3.inDST()); // Does this timezone ever use daylight-savings? console.log("s3.hasDST(): " + s3.hasDST()); // The current, DST-aware time-difference from UTC, in hours console.log("s3.offset(): " + s3.offset()); // Checks if the current time is between 10pm and 8am console.log("s3.isAsleep(): " + s3.isAsleep());
As well as with manipulation methods:
// s5: 2020-01-10 1:31pm // Move to the first millisecond of the day, week, month, year, etc. const s11 = s5.startOf('month'); // 2020-01-01 12:00am // Move to the last millisecond of the day, week, month, year, etc. const s12 = s5.endOf('week'); // 2020-01-12 11:59pm // Increment the date/time by a number and unit const s13 = s5.add(1, 'season') // 2020-05-10 1:31pm // Decrease the date/time by a number and unit const s14 = s5. subtract(2, 'years') // 2018-01-10 1:31pm // Move forward/backward to the closest unit const s15 = s5.nearest('hour'); // 2020-01-10 2:00pm // Go to the beginning of the next unit const s16 = s5.next('quarter'); // 2020-01-04 12:00am // Go to the beginning of the previous unit const s17 = s5.last('month'); // 2019-12-01 12:00am
To format dates and times, Spacetime has some predefined formats:
s11.format('numeric-uk') // 01/01/2020 s12.format('iso-utc') // 2020-01-13T05:59:59.999Z s13.format('mm/dd') // 05/10 s14.format('nice') // Jan 10th, 1:31pm s15.format('quarter') // Q1 // They can be combined using this syntax s16.format('{day} {date-ordinal}, {month-short} {year}')); // Sunday 1st, Dec 2019
But you can also use more standard date format patterns:
s17.unixFmt('yyyy/MM/dd h a')); // 2019/12/01 12 AM
Or the since()
function for relative times. For example, when you execute:
spacetime('September 1 2020').since('September 30 2020')
It will return the following object:
{ "diff": { "years": 0, "months": 0, "days": -29, "hours": 0, "minutes": 0, "seconds": 0 }, "rounded": "in 29 days", "qualified": "in 29 days", "precise": "in 29 days" }
About time zones, when you create an instance, you can pass an additional parameter to specify the time zone (the use of IANA names is recommended):
const s18 = spacetime(1601521200000, "Europe/Paris"); const s19 = spacetime([2020, 0, 1, 20, 0], "Lima"); // America/Lima const s20 = spacetime.now("-4h");
But once you have an instance, you can also easily change it to another time zone with goto()
(once again, the use of IANA names is recommended):
const s21 = s18.goto("Australia/Sydney"); // Oct 1 2020, 1:00pm const s22 = s18.goto("GMT-5"); //-5 is actually +5
Also, you can get an array containing all the time zones within a range of hours using your local time as a reference:
spacetime.whereIts('12:00pm', '2:00pm') // ["asia/seoul", "asia/tokyo", "pacific/palau", "australia/adelaide", ...] spacetime.whereIts('10am') // Within an hour, from 10am to 11am
And get the metadata of the time zone of an instance:
s21.timezone(); /* Returns: { "name": "Australia/Sydney", "hasDst": true, "default_offset": 10, "hemisphere": "South", "current": { "offset": 10, "isDST": false }, "change": { "start": "04/05:03", "back": "10/04:02" } } */
You can try all the examples in this sandbox.
One of the most helpful features of Moment.js is the ability to display dates as relative time:
const start = moment('2020-12-01'); const end = moment('2020-12-04'); start.to(end); // "in 3 days" start.to(end, true); // "3 days"
Not all libraries provide this functionality, but now that Intl.RelativeTimeFormat
is fully supported by most browsers, this is not a problem anymore.
Intl.RelativeTimeFormat
is a standard built-in object that allows you to format numbers as relative times in a localized way.
The constructor optionally takes two arguments, a BCP 47 language tag (or an array of such strings) and an object with properties to configure the locale matching algorithm, the format, and the length of the output message:
const rtf = new Intl.RelativeTimeFormat("es", { localeMatcher: "best fit", numeric: "auto", style: "short" });
Once you have an instance of this object, you can use the format()
method passing the numeric value to use in the message as the first argument, and the unit (like days or hours, in either singular or plural forms) as the second argument:
rtf.format(-4, 'second'); // hace 4 s rtf.format(-1, 'week'); // la semana pasada rtf.format(3, 'quarter'); // dentro de 3 trim. rtf.format(2, 'year'); // dentro de 2 a
Or formatToParts()
to get an array with the parts of the message separated:
rtf.formatToParts(-4, 'second'); /* Returns: [ { "type": "literal", "value": "hace " }, { "type": "integer", "value": "4", "unit": "second" }, { "type": "literal", "value": " s" } ] **/
And if you’re wondering how to get the numeric part, in other words, the elapsed time between two dates, check out this StackOverflow answer that shares the following function:
// in miliseconds var units = { year : 24 * 60 * 60 * 1000 * 365, month : 24 * 60 * 60 * 1000 * 365/12, day : 24 * 60 * 60 * 1000, hour : 60 * 60 * 1000, minute: 60 * 1000, second: 1000 }; var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); var getRelativeTime = (d1, d2 = new Date()) => { var elapsed = d1 - d2; // "Math.abs" accounts for both "past" & "future" scenarios for (var u in units) if (Math.abs(elapsed) > units[u] || u == 'second') return rtf.format(Math.round(elapsed/units[u]), u); }
You can try all the examples in this sandbox.
There’s no doubt there are a lot of alternatives to Moment.js. In this article, we have reviewed three libraries that provide similar functionality and a few useful additions in some cases.
In my opinion, each of those libraries is better at different use cases:
Also, now the JavaScript Internationalization API is more widely supported by browsers, consider the combination of a lightweight library and native features or even if you need an external library at all.
Happy coding!
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.