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

bash
npm install dayjs

Output: added dayjs to dependencies

bash
pnpm add dayjs

Output: added 1 package, linked from store

bash
yarn add dayjs

Output: added dayjs

bash
bun add dayjs

Output: installed dayjs

bash
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.x line 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:

javascript
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/Node Intl.DateTimeFormat)
  • relativeTime — "5 minutes ago" / "in 2 hours"
  • customParseFormat — parse strings with explicit format tokens
  • duration — duration arithmetic
  • isBetween, isSameOrBefore, isSameOrAfter — comparison helpers
  • weekOfYear, isoWeek — ISO week numbering
  • localizedFormat — 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

PackageTrade-off
date-fnsTree-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.
luxonSuccessor 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-jodaPort 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

  1. Immutable — add() returns a new instance. const d = dayjs(); d.add(1, "day") does NOT mutate d. Reassign: d = d.add(1, "day"). Easy slip when migrating Moment code, which was mutable.
  2. Plugin order matters for timezone work. timezone depends on utc. Always extend(utc) first, then extend(timezone). Forgetting yields silent wrong-result behaviour, not an error.
  3. 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. Use customParseFormat plus an explicit token string for any user-supplied input.
  4. Locale must be loaded before use. dayjs.locale("de") without first importing dayjs/locale/de silently falls back to English. There is no warning.
  5. Timezone plugin depends on Intl data. Node bundles full ICU since 13.x, so this works out of the box. Slim Node builds (Alpine docker images using node:alpine without ICU) only support en-US, which breaks timezone offsets for non-US zones. Install full-icu or use a non-slim base image.
  6. Comparison via === does NOT work. Each dayjs() call produces a fresh object even with the same input. Use a.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.

typescript
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:

text
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.

typescript
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:

text
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.

typescript
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):

text
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.

typescript
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:

text
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.

typescript
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:

text
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.

dockerfile
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.

typescript
// 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.

ChangeReleased aroundImpact
Default parser stricter for "YYYY-MM-DD"1.11.xSome 8-digit numeric strings stopped parsing without customParseFormat.
timezone plugin reworked to depend on Intl only1.10.xRemoved bundled tzdata; bundle size dropped, but slim Node images now need full-ICU.
relativeTime thresholds API1.9.xCustom thresholds now declared per-unit with { l, r, d }.
Locale weekStart semantics1.8.xSome locales changed first-day-of-week. Audit startOf("week") outputs.
ESM build path1.11.xdayjs/esm/index.js exposed; modern bundlers auto-pick.

Moving off Moment to Day.js:

  1. Replace moment imports with dayjs.
  2. Convert mutating calls — m.add(1, "day") becomes d = d.add(1, "day").
  3. Install plugins for any non-core method you used (utc, timezone, duration, relativeTime, customParseFormat, localizedFormat, weekOfYear).
  4. Replace locale imports — require("moment/locale/de") becomes import "dayjs/locale/de".
  5. Search for .tz() / .utc() and verify the plugin chain is extended.
  6. 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.

StyleImport
ESMimport dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import "dayjs/locale/de";
CJSconst dayjs = require("dayjs"); const utc = require("dayjs/plugin/utc"); require("dayjs/locale/de");
TypeScriptIdentical to ESM. Types ship in-tree; no @types/dayjs.
ViteBoth ESM and CJS work. Vite picks the ESM build by default.
Webpack 5Same. May need resolve.mainFields if the project pins old fields.
esbuild / tsup / rollupNative ESM consumption. No special config.
Cloudflare WorkersWorks — 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.
BunWorks 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.

PluginPurpose
dayjs/plugin/utcUTC-mode conversion (.utc(), .local()). Prerequisite for timezone.
dayjs/plugin/timezoneIANA timezone support via Intl.DateTimeFormat.
dayjs/plugin/relativeTime"5 minutes ago", "in 2 hours".
dayjs/plugin/customParseFormatStrict parse with explicit format tokens.
dayjs/plugin/localizedFormatLocale-aware L, LL, LLL, LLLL format tokens.
dayjs/plugin/advancedFormatExtra tokens: Q, Do, k, kk, X, x, z, zzz.
dayjs/plugin/durationDuration arithmetic — dayjs.duration(2, "hours").
dayjs/plugin/isBetween, isSameOrBefore, isSameOrAfterBoundary-inclusive comparisons.
dayjs/plugin/weekOfYear, isoWeekISO week-number support.
dayjs/plugin/quarterOfYearQuarter arithmetic.
dayjs/plugin/dayOfYear1-366 day-of-year accessor.
dayjs/plugin/weekdayLocale-aware first-day-of-week.
dayjs/plugin/objectSupportConstruct 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/dayjsNot 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().

typescript
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.

bash
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:

typescript
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.

yaml
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() === false early.
  • Unbounded loops on extreme inputs. A loop like while (d.isBefore(end)) d = d.add(1, "day") runs forever if end is the epoch 0. 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 functiontimezone 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. luxon was 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/polyfill is 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.DateTimeFormat is 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-fns exports 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-joda ports the Java 8 java.time API to JS. Verbose but mathematically precise for engineers who prefer that model.

See also