cheat sheet
dayjs
Package-level reference for the Day.js date library on npm — install, plugin model, version policy, and alternatives.
dayjs
What it is
dayjs is a ~2 kB minified+gzipped JavaScript date library with a Moment.js-compatible API and a plugin-based architecture. Anything not in the core (timezone, relative-time, advanced parsing) lives in a separately-imported plugin so the default bundle stays tiny.
Reach for dayjs when you want a drop-in replacement for legacy moment code or a tiny, immutable wrapper over Date. Reach for date-fns for tree-shakeable pure functions, luxon for first-class timezone handling, or the upcoming standards-based Temporal API for greenfield projects.
Install
npm install dayjs
Output: added dayjs to dependencies
pnpm add dayjs
Output: added 1 package, linked from store
yarn add dayjs
Output: added dayjs
bun add dayjs
Output: installed dayjs
deno add npm:dayjs
Output: added npm:dayjs to import map
Versioning & Node support
Current line is dayjs@1.x. The maintainers kept the 1.x versioning across many years for ecosystem stability — minor releases add plugins and locales, never break the public surface.
- Node: any LTS (works on 14+ but realistically tested on 18+).
- Browsers: ES5 build available; modern ESM build via
dayjs/esm/*. - Dual ESM/CJS via conditional exports.
- TypeScript types ship in-tree (no
@types/dayjs). - A
dayjs@2.xline has been discussed publicly but not released; expect plugin-loading and locale-resolution changes when it lands.
Package metadata
- Maintainer: iamkun + Day.js contributors
- Project home: github.com/iamkun/dayjs
- Docs: day.js.org
- npm: npmjs.com/package/dayjs
- License: MIT
- First released: 2018
- Downloads: ~25 million weekly downloads — pulled in by Ant Design, Element Plus, Naive UI, and any project migrating off Moment.
Peer dependencies & extras
dayjs is a single zero-dep package. No peer-deps, no extras flag — every "extra" is a plugin imported from inside the package itself:
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
dayjs.extend(customParseFormat);
Output: plugins registered globally on the dayjs default export
Commonly-used plugins:
utc+timezone— UTC conversion and IANA timezone handling (depends on browser/NodeIntl.DateTimeFormat)relativeTime— "5 minutes ago" / "in 2 hours"customParseFormat— parse strings with explicit format tokensduration— duration arithmeticisBetween,isSameOrBefore,isSameOrAfter— comparison helpersweekOfYear,isoWeek— ISO week numberinglocalizedFormat— locale-aware token formatting
Locales live under dayjs/locale/<code> and are loaded the same way: import "dayjs/locale/de"; then dayjs.locale("de").
Alternatives
| Package | Trade-off |
|---|---|
date-fns | Tree-shakeable pure functions (addDays(date, 1) rather than date.add(1, "day")). ~13 kB if you use a lot; near-zero if you use little. No mutating API. Pairs well with TypeScript. |
luxon | Successor to Moment by the same maintainer. First-class IANA timezones via Intl. Larger bundle (~70 kB min) but no plugins to remember. |
moment (legacy) | Still works, but officially "in maintenance" — the team recommends migrating. Mutable API, large bundle (~70 kB min). |
js-joda | Port of Java's java.time — strict immutable types (LocalDate, Instant, ZonedDateTime). Best for projects that already use the Java model. |
Native Temporal (stage 3) | TC39 stage-3 proposal at the time of writing. Already in Firefox Nightly + polyfilled via @js-temporal/polyfill. Eventually replaces all of the above; not yet production-ready everywhere. |
Common gotchas
- Immutable —
add()returns a new instance.const d = dayjs(); d.add(1, "day")does NOT mutated. Reassign:d = d.add(1, "day"). Easy slip when migrating Moment code, which was mutable. - Plugin order matters for timezone work.
timezonedepends onutc. Alwaysextend(utc)first, thenextend(timezone). Forgetting yields silent wrong-result behaviour, not an error. - Default parser is strict-ish but not fully ISO.
dayjs("2026/01/02")works, but ambiguous formats like"01-02-2026"parse inconsistently across runtimes. UsecustomParseFormatplus an explicit token string for any user-supplied input. - Locale must be loaded before use.
dayjs.locale("de")without first importingdayjs/locale/desilently falls back to English. There is no warning. - Timezone plugin depends on
Intldata. Node bundles full ICU since 13.x, so this works out of the box. Slim Node builds (Alpine docker images usingnode:alpinewithout ICU) only supporten-US, which breaks timezone offsets for non-US zones. Installfull-icuor use a non-slim base image. - Comparison via
===does NOT work. Eachdayjs()call produces a fresh object even with the same input. Usea.isSame(b),a.isBefore(b), etc.
Real-world recipes
These recipes focus on package-level pain points: timezone gymnastics, business-day arithmetic, and locale fallback at scale. The companion article covers basic parsing and formatting.
Cross-timezone scheduling
Show a "meeting at 14:00 New York" to viewers in Tokyo and London without losing accuracy across DST transitions. The timezone plugin defers the actual zone math to Intl.DateTimeFormat, so the IANA database is always current with the host runtime.
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
const meetingNY = dayjs.tz("2026-06-15 14:00", "America/New_York");
console.log("NY: ", meetingNY.format("YYYY-MM-DD HH:mm z"));
console.log("Tokyo: ", meetingNY.tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm z"));
console.log("London:", meetingNY.tz("Europe/London").format("YYYY-MM-DD HH:mm z"));
console.log("UTC: ", meetingNY.utc().format("YYYY-MM-DDTHH:mm:ss[Z]"));
Output:
NY: 2026-06-15 14:00 EDT
Tokyo: 2026-06-16 03:00 JST
London: 2026-06-15 19:00 BST
UTC: 2026-06-15T18:00:00Z
The z format token requires advancedFormat; if you see literal z instead of the abbreviation, dayjs.extend(advancedFormat).
Business-day calculation
"Add 3 business days" is not a built-in — but the dayjs-business-days community plugin or a 10-line helper does the job. The helper version below avoids the dep.
import dayjs, { Dayjs } from "dayjs";
function addBusinessDays(start: Dayjs, days: number): Dayjs {
let d = start;
let remaining = days;
const step = days > 0 ? 1 : -1;
while (remaining !== 0) {
d = d.add(step, "day");
const dow = d.day();
if (dow !== 0 && dow !== 6) remaining -= step;
}
return d;
}
const start = dayjs("2026-05-29");
console.log(start.format("ddd YYYY-MM-DD"));
console.log(addBusinessDays(start, 3).format("ddd YYYY-MM-DD"));
console.log(addBusinessDays(start, -5).format("ddd YYYY-MM-DD"));
Output:
Fri 2026-05-29
Wed 2026-06-03
Fri 2026-05-22
For holiday-aware arithmetic, install dayjs-business-days or @joycode-pro/dayjs-business-days — both accept an array of holiday dates and skip them in addition to weekends.
Locale fallback at runtime
Loading every locale at startup bloats the bundle. The pattern below dynamic-imports a locale on demand and falls back to English if the chunk fails to load.
import dayjs from "dayjs";
async function setLocale(code: string) {
try {
await import(`dayjs/locale/${code}.js`);
dayjs.locale(code);
} catch {
console.warn(`Locale ${code} unavailable; falling back to en`);
dayjs.locale("en");
}
}
await setLocale(navigator.language?.split("-")[0] ?? "en");
console.log(dayjs().format("dddd, LL"));
Output (de):
Sonntag, 31. Mai 2026
In a Vite or Webpack build, the dynamic import(\dayjs/locale/${code}.js`)` produces a per-locale chunk — only the requested locales ship to the browser.
Strict parsing of user input
The default parser is lenient. For form input and API payloads, opt into strict mode via customParseFormat.
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(customParseFormat);
const samples = ["2026-05-31", "31/05/2026", "May 31, 2026", "garbage"];
for (const s of samples) {
const parsed = dayjs(s, ["YYYY-MM-DD", "DD/MM/YYYY", "MMMM D, YYYY"], true);
console.log(s, "→", parsed.isValid() ? parsed.format("YYYY-MM-DD") : "invalid");
}
Output:
2026-05-31 → 2026-05-31
31/05/2026 → 2026-05-31
May 31, 2026 → 2026-05-31
garbage → invalid
The true flag forces strict matching against the supplied format list; without it the parser falls back to lenient guesses.
Relative time with custom thresholds
relativeTime says "a few seconds ago" by default. Override the thresholds for finer or coarser granularity.
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime, {
thresholds: [
{ l: "s", r: 1 },
{ l: "ss", r: 59, d: "second" },
{ l: "m", r: 1 },
{ l: "mm", r: 59, d: "minute" },
{ l: "h", r: 1 },
{ l: "hh", r: 23, d: "hour" },
{ l: "d", r: 1 },
{ l: "dd", r: 29, d: "day" },
{ l: "M", r: 1 },
{ l: "MM", r: 11, d: "month" },
{ l: "y" },
{ l: "yy", d: "year" },
],
});
console.log(dayjs().subtract(45, "second").fromNow());
console.log(dayjs().subtract(3, "hour").fromNow());
console.log(dayjs().subtract(2, "year").fromNow());
Output:
45 seconds ago
3 hours ago
2 years ago
Production deployment
Day.js is a runtime library — no separate build step. Deployment considerations are about ICU data, locale bundling, and timezone-database freshness.
Full-ICU in Docker
Slim Node base images (node:20-alpine, node:20-slim) ship without full ICU data. Without it, Intl.DateTimeFormat only supports en-US, which breaks the timezone plugin and localizedFormat outputs.
FROM node:20-slim
# Option A: install full ICU at the system level
RUN apt-get update && apt-get install -y --no-install-recommends \
libicu-dev && rm -rf /var/lib/apt/lists/*
# Option B: use the npm package
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm i full-icu
ENV NODE_ICU_DATA=/app/node_modules/full-icu
COPY . .
CMD ["node", "server.js"]
Verify with node -e "console.log(new Intl.DateTimeFormat('de').format(new Date()))" — non-English output confirms full ICU.
Timezone database freshness
Day.js doesn't ship its own zone data — it reads from the host runtime's Intl. The relevant tzdata version is:
- Node 22+ → the bundled ICU is refreshed each Node minor.
- Browsers → whatever the OS / browser provides. Usually within months of the latest IANA release.
- Cloudflare Workers → workerd updates ICU regularly; safer than self-hosted runtimes.
If a DST rule changed in the past 60 days, prefer redeploying on a newer Node version rather than carrying a custom tz dataset.
Bundle size in front-end builds
Each plugin and locale you import is included in the bundle. Audit with vite build --report or webpack-bundle-analyzer. A typical web app pulling utc, timezone, customParseFormat, relativeTime plus one locale adds ~6 KB gzipped — still a fraction of Moment.
// Tree-shake-friendly: import only what you use
import dayjs from "dayjs";
import "dayjs/locale/de";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(customParseFormat);
dayjs.locale("de");
Server-side rendering
Day.js has no SSR-specific hazards — every call creates an immutable instance keyed off the host clock. The risk is mismatched timezones between server and client: render dayjs().format(...) server-side and the client may show a different value if the user's browser zone differs. Either render in UTC and let the client re-format, or accept an "as of " caption.
Version migration guide
Day.js's 1.x line has been remarkably stable — most "migrations" are plugin or locale changes, not core breaks. The team has discussed a 2.x but not shipped one at the time of writing; the items below are what changed within 1.x.
| Change | Released around | Impact |
|---|---|---|
Default parser stricter for "YYYY-MM-DD" | 1.11.x | Some 8-digit numeric strings stopped parsing without customParseFormat. |
timezone plugin reworked to depend on Intl only | 1.10.x | Removed bundled tzdata; bundle size dropped, but slim Node images now need full-ICU. |
relativeTime thresholds API | 1.9.x | Custom thresholds now declared per-unit with { l, r, d }. |
Locale weekStart semantics | 1.8.x | Some locales changed first-day-of-week. Audit startOf("week") outputs. |
| ESM build path | 1.11.x | dayjs/esm/index.js exposed; modern bundlers auto-pick. |
Moving off Moment to Day.js:
- Replace
momentimports withdayjs. - Convert mutating calls —
m.add(1, "day")becomesd = d.add(1, "day"). - Install plugins for any non-core method you used (
utc,timezone,duration,relativeTime,customParseFormat,localizedFormat,weekOfYear). - Replace locale imports —
require("moment/locale/de")becomesimport "dayjs/locale/de". - Search for
.tz()/.utc()and verify the plugin chain is extended. - Run a regression on every date-related test; the date math is identical but the strict-mode parser may reject inputs Moment accepted.
Considering Temporal: the TC39 Temporal API is at stage 3 and polyfilled via @js-temporal/polyfill. It replaces both Day.js and Moment with a standards-based, immutable, timezone-first API. New greenfield projects should evaluate Temporal before committing to Day.js — but Day.js is still the safer pick for short-term needs because the polyfill is ~20 KB and the native implementation is not yet shipped in evergreen browsers.
ESM/CJS interop & bundling
Day.js dual-publishes ESM and CJS. The interop is normally invisible, but plugin paths and locale paths differ between the two.
| Style | Import |
|---|---|
| ESM | import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import "dayjs/locale/de"; |
| CJS | const dayjs = require("dayjs"); const utc = require("dayjs/plugin/utc"); require("dayjs/locale/de"); |
| TypeScript | Identical to ESM. Types ship in-tree; no @types/dayjs. |
| Vite | Both ESM and CJS work. Vite picks the ESM build by default. |
| Webpack 5 | Same. May need resolve.mainFields if the project pins old fields. |
| esbuild / tsup / rollup | Native ESM consumption. No special config. |
| Cloudflare Workers | Works — pure JS, no native deps. The timezone plugin requires ICU, which workerd provides. |
Deno (npm:dayjs) | ESM only. Plugin imports use npm:dayjs/plugin/utc. |
| Bun | Works in both modes. |
Common interop trap: mixing ESM and CJS plugins. If your app is ESM but a CJS dep registers a plugin (dayjs.extend(utc)) on a separate dayjs instance, the two instances don't share extensions. Always import dayjs from the same canonical module — never via deep paths like dayjs/dist/dayjs.min.js.
Plugin & ecosystem coverage
Day.js's core surface is intentionally small. Every advanced feature is a plugin imported separately.
| Plugin | Purpose |
|---|---|
dayjs/plugin/utc | UTC-mode conversion (.utc(), .local()). Prerequisite for timezone. |
dayjs/plugin/timezone | IANA timezone support via Intl.DateTimeFormat. |
dayjs/plugin/relativeTime | "5 minutes ago", "in 2 hours". |
dayjs/plugin/customParseFormat | Strict parse with explicit format tokens. |
dayjs/plugin/localizedFormat | Locale-aware L, LL, LLL, LLLL format tokens. |
dayjs/plugin/advancedFormat | Extra tokens: Q, Do, k, kk, X, x, z, zzz. |
dayjs/plugin/duration | Duration arithmetic — dayjs.duration(2, "hours"). |
dayjs/plugin/isBetween, isSameOrBefore, isSameOrAfter | Boundary-inclusive comparisons. |
dayjs/plugin/weekOfYear, isoWeek | ISO week-number support. |
dayjs/plugin/quarterOfYear | Quarter arithmetic. |
dayjs/plugin/dayOfYear | 1-366 day-of-year accessor. |
dayjs/plugin/weekday | Locale-aware first-day-of-week. |
dayjs/plugin/objectSupport | Construct from { year, month, day, hour, minute, second } literals. |
dayjs/plugin/calendar | "Yesterday at 14:00" style formatting (Moment compatibility). |
dayjs/plugin/timezoneArithmetic (community) | Add days respecting a target timezone — not a built-in. |
dayjs-business-days (community) | Holiday-aware business-day arithmetic. |
@types/dayjs | Not needed — types are in-tree. Installing it overrides with stale community types. |
Locales live under dayjs/locale/<code> — currently 140+ shipped. The full list is in the README and updated with each minor release.
Testing & CI integration
Date logic is one of the easiest places for test flakiness — Day.js gives you levers to keep tests deterministic.
Freeze "now" in tests
Vitest and Jest both ship vi.useFakeTimers() / jest.useFakeTimers(). Day.js respects them because it ultimately calls new Date().
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import dayjs from "dayjs";
describe("birthday", () => {
beforeEach(() => vi.useFakeTimers().setSystemTime(new Date("2026-05-31T12:00:00Z")));
afterEach(() => vi.useRealTimers());
it("knows today", () => {
expect(dayjs().format("YYYY-MM-DD")).toBe("2026-05-31");
});
});
Output: 1 passed.
Test timezone-sensitive code with TZ env
The host process's TZ env var controls the default timezone for new Date(). Set it inside the test command for reproducibility.
TZ=UTC npx vitest run
Output: all dayjs().tz() defaults to UTC, eliminating CI-vs-laptop drift.
Locale reproducibility
Locale plugins register globally. If a previous test changed the locale (dayjs.locale("de")), subsequent tests inherit it. Reset in an afterEach:
afterEach(() => dayjs.locale("en"));
CI matrix on Node ICU variants
The timezone plugin relies on host ICU. Run CI on both node:20-slim (with full-icu) and node:20 (full ICU bundled) to catch ICU regressions.
strategy:
matrix:
node: [20, 22]
icu: [bundled, full-icu]
Security considerations
Day.js itself has no I/O, no eval, no native deps — its security surface is essentially zero. Risks are around inputs.
- Untrusted format strings. Day.js's format parsers don't execute user input as code, but malformed tokens silently produce wrong dates. Validate before parsing; reject
isValid() === falseearly. - Unbounded loops on extreme inputs. A loop like
while (d.isBefore(end)) d = d.add(1, "day")runs forever ifendis the epoch0. Cap iterations or bound the range explicitly. - Timezone abbreviation injection. Some libraries display
dayjs.tz.guess()directly. The IANA name is safe, but a user-supplied tz string passed to.tz(input)can throw if invalid. Wrap in try/catch and fall back to UTC. - Daylight-saving edge cases.
dayjs.tz("2026-03-08 02:30", "America/New_York")lands on a non-existent local time. The result is the next valid instant — but for billing / scheduling logic, you may want to reject the input. Verify via.format() === input. - Dependency pin. Day.js is one of the few date libraries small enough to fully audit; the package is ~30 source files. Pin the exact minor in production and review release notes — historical minors have changed parsing semantics.
Troubleshooting common errors
TypeError: dayjs.tz is not a function — timezone plugin not loaded. Add dayjs.extend(timezone) (after dayjs.extend(utc)).
Invalid Date when calling .format() — input failed to parse. Check d.isValid() before formatting. Common causes: ambiguous date string without customParseFormat, empty string, undefined.
Timezone offset is wrong on Alpine Docker — slim image without full ICU. See "Production deployment" above; install full-icu or switch to the non-slim base.
Cannot read property 'months' of undefined — locale not loaded. dayjs.locale("de") requires import "dayjs/locale/de" first.
d.add(1, "day") returns the same date — you forgot to reassign. Day.js is immutable: d = d.add(1, "day").
.diff() returns a negative number unexpectedly — argument order. a.diff(b, "day") is a - b. Swap arguments for a positive result.
Strict-mode parsing rejects valid input — format token mismatch. Common slip: "MM-DD-YYYY" vs "MM/DD/YYYY". Provide an array of allowed formats: dayjs(s, ["YYYY-MM-DD", "MM/DD/YYYY"], true).
.format("Z") returns "+00:00" for everything — running inside a container with TZ=UTC or Node started without timezone-aware ICU. Set the container's TZ to a real zone or call .tz("America/New_York") explicitly.
When NOT to use this
Skip Day.js when:
- You need heavy duration math, intervals, or full-fat timezone semantics.
luxonwas designed for that workload — its core API includes durations, intervals, and DateTime, all immutable, no plugins. Bundle is bigger (~50 KB) but the ergonomics pay off. - You're starting a greenfield project and can use Temporal. The
@js-temporal/polyfillis production-usable today. Once native Temporal ships in major browsers (estimated 2026-2027), Day.js becomes a transitional dependency. - You only format dates once, with simple tokens.
Intl.DateTimeFormatis built into the runtime and handles ~80 % of formatting needs without any dependency.new Date().toLocaleString("en", { dateStyle: "medium" }). - You write date logic in pure functions and care about tree-shaking.
date-fnsexports individual functions you can import à la carte. A typical app pulls 3 KB rather than Day.js's ~6 KB after plugins. - You depend on a framework that already includes a date library. Don't double up. Next.js / Nuxt don't ship one; Vue Element / Ant Design do — they already bundle Day.js.
- You're in a Java-port mindset.
js-jodaports the Java 8java.timeAPI to JS. Verbose but mathematically precise for engineers who prefer that model.
See also
- JavaScript: dayjs — formatting, parsing, plugin tutorials
- Concept: json — date serialization across API boundaries