Day.js: a 2.99 KB core, and locales and plugins you import yourself
⏰ Day.js 2kB immutable date-time library alternative to Moment.js with the same modern API
At a glance
- What is it?
- Day.js gives Moment-style date handling in a few kilobytes, and pays for that with a hard size gate, opt-in locales and plugins, and a repository that never maps Moment methods to the plugin that now holds them.
- Who is it for?
- Day.js fits teams that want Moment-shaped code without Moment's download, and it is a poor fit for anyone who needs the whole date surface in one import or wants to confirm the all-browsers claim locally. Before adopting it on an existing Moment codebase, list every format token and locale your app actually uses, then check each one against the plugin list on day.js.org.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 15 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A 2.99 KB size-limit gate decides what stays in the core
The 2kB figure in Day.js's own package.json description is enforced mechanically rather than promised. The build script is `cross-env BABEL_ENV=build node build && npm run size`, and `npm run size` runs `size-limit && gzip-size dayjs.min.js`. A size-limit entry in package.json caps `dayjs.min.js` at 2.99 KB, so a change that pushes the minified core past that ceiling fails the build instead of shipping.
This is the most consequential mechanism in the repository, because it decides what can be built in and what has to ship as a separate module. Anything a user does not import on purpose stays outside that budget, which is the mechanical reason locales and plugins are individual files you load by hand. The consequence for a reader: if you cannot find a formatting token or a locale in the core, the answer is not that Day.js lacks the capability, but that putting it in the core would break this gate. Anything that does not fit has to arrive as a plugin or a locale file, and your application is the thing that decides whether it arrives at all.
Locales and plugins stay out of the bundle until you import them
Internationalization is opt-in twice over, once for the data and once for the registration. The three lines Day.js gives are the whole mechanism:
import 'dayjs/locale/es' // load on demand
dayjs.locale('es') // use Spanish locale globally
dayjs('2018-05-05').locale('zh-cn').format() // use Chinese Simplified locale in a specific instanceNo locale is in your build unless you use it, so calling `dayjs.locale('es')` after forgetting the import does not stop the format call from running. The output simply stays in whatever locale the instance already carries. Plugins work the same way, with a second step that is easy to miss:
import advancedFormat from 'dayjs/plugin/advancedFormat' // load on demand
dayjs.extend(advancedFormat) // use plugin
dayjs().format('Q Do k kk X x') // more available formats`dayjs.extend(advancedFormat)` has to run before the format string, and the import has to run before the extend. Skip either step and the rest of your format string still renders, so the failure is a plausible-looking date with the wrong fields rather than an exception. If a locale or a token matters to your product, that import belongs in code review next to the code that formats, not in a setup document nobody rereads.
An immutable set() and locale() mean you keep the value that comes back
Day.js describes itself as immutable and chainable, and the get-and-set line shows what that means:
dayjs().set('month', 3).month() // get & setThe set call hands back an object, and the month read runs on that returned object rather than on the one you started from. The same shape runs through the headline example, where each link depends on the previous one having produced a new value: `dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:mm:ss')`. Parse, display, manipulation and query cover the rest of the surface: `dayjs('2018-08-08')`, `dayjs().format('{YYYY} MM-DDTHH:mm:ss SSS [Z] A')`, `dayjs().add(1, 'year')` and `dayjs().isBefore(dayjs())`.
For a reader arriving from a mutable date library, the failure mode is silent. Code that calls add and then reads a field from the original variable runs to completion and produces the original value, because the addition happened on a different object. The per-instance locale switch behaves the same way: `dayjs('2018-05-05').locale('zh-cn').format()` formats in Chinese, and the instance you passed in is unchanged. Chain the calls, or store what they return.
The same jest files run under four TZ values before coverage is counted
Time zone handling is checked by repeating identical runs in different environments rather than by a separate suite. The `test-tz` script is `date && jest test/timezone.test --coverage=false`, and the `test` script chains it under `cross-env TZ=Pacific/Auckland`, `cross-env TZ=Europe/London`, `cross-env TZ=America/Whitehorse` and once with the ambient zone, then repeats four plugin runs under `Europe/Paris`, `Europe/London`, `America/New_York` and `Pacific/Auckland` before the coverage run.
The consequence for someone reading the repository is that time zone confidence here is a property of the host, not a bundled data file. No zone database appears in the top-level entries or in package.json, and the only plugin worked through in full is advancedFormat, which adds format tokens rather than zone conversion. A reader who needs UTC or IANA zone handling therefore has no in-repo starting point: the plugin list on day.js.org is the next place to look. The four time zone runs in the default `npm test` show the area is exercised on every change, without telling you which module serves it.
src/ has to reach 100 percent line coverage or npm test fails
Coverage is not advisory in this repository. The jest config sets `collectCoverage` to true, points `collectCoverageFrom` at `src/**/*`, restricts `roots` to `test`, matches files with a `testRegex` of `test/(.*?/)?.*test.js$` and sets a `testURL` of `http://localhost`. The last command in the `test` script is `jest --coverage --coverageThreshold="{ \"global\": { \"lines\": 100} }"`, a global 100 percent line threshold, so one uncovered line under `src/` fails the run.
Alongside that, a `pre-commit` hook runs `lint`, which is `./node_modules/.bin/eslint src/* test/* build/*` over the same three directories, and the tree carries `.eslintrc.json`, `prettier.config.js` and `.editorconfig`. For a contributor the split is predictable: a branch added without a matching test in `test/` passes lint and fails coverage. The version question is less well served, because neither the installation snippet nor those scripts name a Node version, so the version you run them on is your own decision.
Browser runs go through karma.sauce.conf.js, and the tree has no local Karma config
The README advertises that all browsers are supported, and the only browser test wiring in the tree is a single file, `karma.sauce.conf.js`. The `sauce` script is `npx karma start karma.sauce.conf.js`, and `test:sauce` runs it four times back to back, passing 0, 1, 2 and 3 after the double dash. No other Karma configuration appears among the top-level entries, and no script starts Karma against a local browser.
The consequence for a reader is that the all-browsers claim is not something this repository lets you check. Exercising the browser suite means driving a remote grid through that config, and no credential file shows up among the top-level entries to make that straightforward. If your application targets a browser the grid does not cover, the unit tests under `test/` still pass and tell you nothing about it. Treat the claim as something to verify against your own support matrix, using the release notes on the repository and a real browser run, rather than as a fact readable from the scripts.
main, types and the version string all resolve to build output
package.json points `main` at `dayjs.min.js` and `types` at `index.d.ts`, and its `version` reads `0.0.0-development`. None of those three names appear among the repository's top-level entries, and neither does the `esm` directory that the `babel` script fills with `babel src --out-dir esm --copy-files`. All of them are produced at build time from `src/`, `types/` and `build/`. That is also why the released tags, v1.11.23 on 2026-08-17, v1.11.22 on 2026-08-16 and v1.11.21 on 2026-05-26, match nothing you can read in the source tree.
The consequence is that the file you would naturally open cannot tell you what version you have, and a `git clone` cannot be pinned to a release without checking out a tag. The same reasoning explains why `.npmignore`, `.releaserc` and `CHANGELOG.md` sit at the top level: what lands in the published tarball and how the version is stamped are separate concerns from the source you read. When you are chasing a behaviour difference between two installs, read the package.json inside the installed copy, not the one in the repository.
The Moment API is familiar, but nothing in the repo maps methods to plugins
The central claim about compatibility is one sentence: if you use Moment.js, you already know how to use Day.js. The bullet list adds a familiar Moment API and patterns, immutable, chainable, I18n support, a 2kb mini library and all browsers supported. What none of that tells you is which part of the Moment surface now lives in a plugin. The only plugin worked through in full is advancedFormat, and its payoff is format tokens: quarters, ordinals, 24 hour hour markers and the unix timestamps `X` and `x` in the string `Q Do k kk X x`.
So for a reader porting an existing Moment codebase, the cost is not the calls you already know, it is the ones you assumed were built in. The repository has no mapping table between Moment methods and Day.js plugins, and the plugin list lives on day.js.org rather than in the tree. The docs directory holds the translated READMEs, at paths such as docs/zh-cn/README.zh-CN.md and docs/ja/README-ja.md, with a `prettier` script that formats docs/**/*.md, so a reader working in another language gets the same statements and the same missing table.
Editorial conclusion
Day.js fits teams that want Moment-shaped code without Moment's download, and it is a poor fit for anyone who needs the whole date surface in one import or wants to confirm the all-browsers claim locally. Before adopting it on an existing Moment codebase, list every format token and locale your app actually uses, then check each one against the plugin list on day.js.org. Before trusting it in production, pin the version you install, because the version in the repository reads 0.0.0-development and tells you nothing about what you got.
Frequently asked questions
Is DayJS better than Moment?
Day.js presents itself as a 2kB alternative to Moment.js with a largely Moment.js-compatible API, MIT licensed, last pushed on 2026-09-15, with releases v1.11.23, v1.11.22 and v1.11.21. It stops at largely compatible, and the plugin list, not the core, is where the rest of the Moment surface has to be looked for.
How do I install dayjs?
The installation line is `npm install dayjs --save`. An Installation Guide lives on day.js.org, and locales and plugins are added afterwards as separate imports rather than as part of that install.
How do I install dayjs plugins?
Import the plugin module and pass it to dayjs.extend: `import advancedFormat from 'dayjs/plugin/advancedFormat'` and then `dayjs.extend(advancedFormat)`. The order matters, since tokens such as those in `Q Do k kk X x` are only handled once the extend call has run.
What is dayjs?
A minimalist JavaScript library that parses, validates, manipulates and displays dates and times for modern browsers, MIT licensed, published from the iamkun/dayjs repository with a dev default branch and documentation on day.js.org.
How do I use dayjs day to day?
Five lines cover the core surface: `dayjs('2018-08-08')` to parse, `dayjs().format('{YYYY} MM-DDTHH:mm:ss SSS [Z] A')` to display, `dayjs().set('month', 3).month()` to get and set, `dayjs().add(1, 'year')` to manipulate and `dayjs().isBefore(dayjs())` to query. Instances are immutable, so you keep what those calls return.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/iamkun-dayjs)