npm install date-fns or npm install dayjs — then find-and-replace Moment patterns using the Quick Reference table below.YYYY-MM-DD but date-fns uses yyyy-MM-dd (lowercase year/day). Day.js uses the same tokens as Moment.YYYY, DD) in date-fns — they produce silent wrong output or throw errorsdayjs.extend() before calling plugin features — unregistered calls throw TypeErrorimport * as dateFns) defeats tree-shaking — always use named imports| Moment.js Pattern | date-fns Equivalent | Day.js Equivalent |
|---|---|---|
moment() | new Date() | dayjs() |
moment('2026-02-23') | parseISO('2026-02-23') | dayjs('2026-02-23') |
moment('12-25-2026', 'MM-DD-YYYY') | parse('12-25-2026', 'MM-dd-yyyy', new Date()) | dayjs('12-25-2026', 'MM-DD-YYYY') (customParseFormat plugin) |
moment().format('YYYY-MM-DD') | format(new Date(), 'yyyy-MM-dd') | dayjs().format('YYYY-MM-DD') |
moment().format('MMMM Do, YYYY') | format(new Date(), 'MMMM do, yyyy') | dayjs().format('MMMM Do, YYYY') (advancedFormat plugin) |
moment().add(7, 'days') | addDays(new Date(), 7) | dayjs().add(7, 'day') |
moment().subtract(1, 'month') | subMonths(new Date(), 1) | dayjs().subtract(1, 'month') |
moment().startOf('month') | startOfMonth(new Date()) | dayjs().startOf('month') |
moment().endOf('month') | endOfMonth(new Date()) | dayjs().endOf('month') |
moment().diff(other, 'days') | differenceInDays(new Date(), other) | dayjs().diff(other, 'day') |
moment().isBefore(other) | isBefore(new Date(), other) | dayjs().isBefore(other) |
moment().isAfter(other) | isAfter(new Date(), other) | dayjs().isAfter(other) |
moment().isSame(other, 'day') | isSameDay(new Date(), other) | dayjs().isSame(other, 'day') |
moment().fromNow() | formatDistance(new Date(), Date.now(), { addSuffix: true }) | dayjs().fromNow() (relativeTime plugin) |
moment().isValid() | isValid(parseISO(str)) | dayjs(str).isValid() |
START
├── Need a Moment.js-compatible chainable API?
│ ├── YES → Use Day.js (near-identical API, 2 KB, plugin-based features)
│ └── NO ↓
├── Need tree-shaking / minimal bundle size?
│ ├── YES → Use date-fns (import only the functions you need, best tree-shaking)
│ └── NO ↓
├── Need first-class time zone support built in?
│ ├── YES → Use date-fns v4 (@date-fns/tz) or Day.js (utc + timezone plugins)
│ └── NO ↓
├── Migrating a large codebase with many moment() calls?
│ ├── YES → Day.js (smallest API surface change — often just rename import)
│ └── NO ↓
├── Prefer functional programming style (no method chaining)?
│ ├── YES → Use date-fns (pure functions, immutable by design)
│ └── NO ↓
├── Targeting only Chrome 144+ / Firefox 139+?
│ ├── YES → Consider Temporal API (native, zero-dependency) with date-fns/Day.js fallback
│ └── NO ↓
└── DEFAULT → date-fns for new projects, Day.js for quick drop-in replacements
Quantify the migration scope by counting Moment.js call sites. This determines whether Day.js (minimal changes) or date-fns (more refactoring, better long-term) is the right target. [src1, src2]
# Count total moment imports/requires
grep -rn "require('moment')\|from 'moment'\|import moment" --include='*.js' --include='*.ts' | wc -l
# Count format() calls (need token conversion for date-fns)
grep -rn '\.format(' --include='*.js' --include='*.ts' | grep -i moment | wc -l
# Count timezone and locale usage
grep -rn 'moment\.tz\|moment-timezone' --include='*.js' --include='*.ts' | wc -l
grep -rn 'moment\.locale\|\.locale(' --include='*.js' --include='*.ts' | wc -l
Verify: If timezone count > 0, plan for @date-fns/tz or dayjs/plugin/timezone. If locale count > 0, plan for locale imports.
Install date-fns or Day.js alongside Moment.js. Both can coexist during the transition. [src3, src4]
# Option A: date-fns
npm install date-fns
npm install @date-fns/tz # if timezone support needed
# Option B: Day.js
npm install dayjs
Verify: node -e "const { format } = require('date-fns'); console.log(format(new Date(), 'yyyy-MM-dd'))" prints today's date.
Build a thin adapter layer so the rest of your codebase imports from one file. This isolates the library choice and simplifies future migrations (e.g., to the Temporal API). [src7]
// utils/date.js — adapter layer
import { format, parseISO, addDays, subDays, differenceInDays,
isBefore, isAfter, startOfMonth, endOfMonth } from 'date-fns';
export const formatDate = (date, fmt = 'yyyy-MM-dd') => format(date, fmt);
export const parseDate = (str) => parseISO(str);
export const addDaysTo = (date, n) => addDays(date, n);
export const daysDiff = (a, b) => differenceInDays(a, b);
Verify: Import and call each exported function in a test file to confirm behavior matches your old Moment.js usage.
Moment.js and date-fns use different format tokens. Day.js uses the same tokens as Moment, so skip this step if using Day.js. [src2, src6]
// Moment.js → date-fns format token conversion
// YYYY → yyyy (4-digit year)
// YY → yy (2-digit year)
// DD → dd (day of month, zero-padded)
// D → d (day of month)
// Do → do (ordinal day: 1st, 2nd, 3rd)
// dddd → EEEE (full weekday name)
// ddd → EEE (abbreviated weekday name)
// A → a (AM/PM)
// X → t (Unix timestamp seconds)
// x → T (Unix timestamp milliseconds)
Verify: For each format string in your codebase, compare moment(date).format(oldFmt) output with dateFns.format(date, newFmt).
Systematically replace Moment calls file by file. Start with utilities and shared code, then work outward to UI components. [src2, src5]
// BEFORE: Moment.js
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');
const nextWeek = moment().add(7, 'days');
// AFTER: date-fns
import { format, addDays } from 'date-fns';
const formatted = format(new Date(), 'yyyy-MM-dd');
const nextWeek = addDays(new Date(), 7);
// AFTER: Day.js
import dayjs from 'dayjs';
const formatted = dayjs().format('YYYY-MM-DD');
const nextWeek = dayjs().add(7, 'day');
Verify: Run your test suite after each file. Compare formatted output strings between old and new implementations.
If your project uses moment-timezone or locale-specific formatting, add the corresponding packages. [src3, src4]
// date-fns v4: timezone support
import { TZDate } from '@date-fns/tz';
import { format } from 'date-fns';
const nyDate = new TZDate(2026, 1, 23, 12, 0, 0, 'America/New_York');
format(nyDate, 'yyyy-MM-dd HH:mm zzz');
// date-fns: locale support
import { de } from 'date-fns/locale';
format(new Date(), 'EEEE, d. MMMM yyyy', { locale: de });
// Day.js: timezone + locale
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc); dayjs.extend(timezone);
dayjs().tz('America/New_York').format('YYYY-MM-DD HH:mm z');
Verify: Compare timezone-converted output with Moment-timezone results across DST boundaries.
Once all usages are replaced and tests pass, uninstall Moment.js. [src1]
npm uninstall moment moment-timezone
# Verify no remaining references
grep -rn "moment" --include='*.js' --include='*.ts' --include='*.tsx' --include='*.json' | grep -v node_modules | grep -v '.md'
Verify: Build succeeds, all tests pass, npm ls moment shows no dependencies. Check bundle size reduction.
Full script: javascript-date-fns-migration-with-full-feature-co.js (34 lines)
// Input: A utility module that wraps all common Moment.js operations
// Output: The same module rewritten with date-fns v4
import {
format, parseISO, parse,
addDays, addMonths, addYears, subDays, subMonths,
differenceInDays, differenceInMonths,
isBefore, isAfter, isSameDay, isValid,
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
formatDistance
} from 'date-fns';
// Parse ISO string (replaces moment('2026-02-23'))
const date = parseISO('2026-02-23');
// Parse custom format (replaces moment('02/23/2026', 'MM/DD/YYYY'))
const customParsed = parse('02/23/2026', 'MM/dd/yyyy', new Date());
// Format (replaces moment().format('MMMM Do, YYYY'))
const formatted = format(new Date(), 'MMMM do, yyyy'); // "February 23rd, 2026"
// Add/subtract (replaces moment().add(7, 'days'))
const nextWeek = addDays(new Date(), 7);
const lastMonth = subMonths(new Date(), 1);
// Difference (replaces moment(a).diff(b, 'days'))
const daysUntil = differenceInDays(parseISO('2026-12-31'), new Date());
// Comparison (replaces moment(a).isBefore(b))
const isPast = isBefore(parseISO('2025-01-01'), new Date()); // true
// Range boundaries (replaces moment().startOf('month'))
const monthStart = startOfMonth(new Date());
// Relative time (replaces moment().fromNow())
const relative = formatDistance(parseISO('2026-01-01'), new Date(), {
addSuffix: true
}); // "about 2 months ago"
// Validation (replaces moment(str).isValid())
const valid = isValid(parseISO('2026-02-30')); // false
Full script: javascript-day-js-migration-with-plugins.js (36 lines)
// Input: A codebase using Moment.js with timezone and relative time
// Output: The same functionality using Day.js with required plugins
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isBetween from 'dayjs/plugin/isBetween';
// Register plugins (one-time setup)
dayjs.extend(customParseFormat);
dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isBetween);
// Parse (identical to Moment.js)
const date = dayjs('2026-02-23');
const custom = dayjs('02/23/2026', 'MM/DD/YYYY'); // requires customParseFormat
// Format (identical tokens to Moment.js)
const formatted = date.format('MMMM Do, YYYY'); // "February 23rd, 2026"
// Add/subtract (returns new instance — immutable unlike Moment)
const nextWeek = dayjs().add(7, 'day');
// Difference (note: singular unit name)
const daysUntil = dayjs('2026-12-31').diff(dayjs(), 'day');
// Relative time (requires relativeTime plugin)
const relative = dayjs('2026-01-01').fromNow();
// Timezone (requires utc + timezone plugins)
const nyTime = dayjs().tz('America/New_York').format('YYYY-MM-DD HH:mm z');
Full script: typescript-type-safe-date-utility-with-date-fns.ts (36 lines)
// Input: Need a type-safe date utility layer for a TypeScript project
// Output: Strongly typed wrapper around date-fns functions
import {
format, parseISO, addDays, subDays,
differenceInDays, isBefore, isAfter, isValid
} from 'date-fns';
type DateInput = Date | string;
function toDate(input: DateInput): Date {
if (typeof input === 'string') {
const parsed = parseISO(input);
if (!isValid(parsed)) {
throw new Error(`Invalid date string: ${input}`);
}
return parsed;
}
return input;
}
function formatDate(input: DateInput, pattern: string = 'yyyy-MM-dd'): string {
return format(toDate(input), pattern);
}
function daysBetween(a: DateInput, b: DateInput): number {
return differenceInDays(toDate(a), toDate(b));
}
function isDateBefore(a: DateInput, b: DateInput): boolean {
return isBefore(toDate(a), toDate(b));
}
// Usage:
const result = formatDate('2026-02-23', 'MMMM do, yyyy'); // "February 23rd, 2026"
const diff = daysBetween('2026-12-31', '2026-02-23'); // 311
const past = isDateBefore('2025-01-01', new Date()); // true
// ❌ BAD — date-fns returns new Date objects, does NOT mutate
const date = new Date('2026-02-23');
addDays(date, 7); // Return value discarded!
console.log(date); // Still 2026-02-23, not 2026-03-02
// ✅ GOOD — Always assign the result
import { addDays } from 'date-fns';
const date = new Date('2026-02-23');
const nextWeek = addDays(date, 7); // New Date: 2026-03-02
console.log(nextWeek); // 2026-03-02
console.log(date); // 2026-02-23 (unchanged)
// ❌ BAD — Moment tokens produce wrong output in date-fns
import { format } from 'date-fns';
format(new Date(), 'YYYY-MM-DD');
// Throws error or wrong output — YYYY and DD are not valid date-fns tokens
// ✅ GOOD — date-fns uses lowercase year/day tokens
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd'); // "2026-02-23"
// Key: YYYY→yyyy, DD→dd, dddd→EEEE, Do→do, A→a
// ❌ BAD — Imports entire library (~70 KB)
import * as dateFns from 'date-fns';
dateFns.format(new Date(), 'yyyy-MM-dd');
// ✅ GOOD — Tree-shakeable, only ~3-6 KB
import { format, addDays, parseISO } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
// ❌ BAD — fromNow() is not in Day.js core
import dayjs from 'dayjs';
dayjs().fromNow(); // TypeError: dayjs(...).fromNow is not a function
// ✅ GOOD — Load plugins before using their features
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs().fromNow(); // "a few seconds ago"
// ❌ BAD — Day.js is immutable
const d = dayjs('2026-02-23');
d.add(7, 'day');
console.log(d.format('YYYY-MM-DD')); // Still "2026-02-23"!
// ✅ GOOD — Day.js returns new instances
const d = dayjs('2026-02-23');
const nextWeek = d.add(7, 'day');
console.log(nextWeek.format('YYYY-MM-DD')); // "2026-03-02"
YYYY for year, date-fns uses yyyy. Using Moment tokens in date-fns causes errors or wrong output. Fix: find-and-replace YYYY→yyyy, DD→dd, dddd→EEEE, Do→do, A→a. [src2]fromNow(), custom parsing, timezone, isBetween(), and ordinals require plugins. Fix: import and dayjs.extend() each plugin at app startup. [src4]import * as dateFns from 'date-fns' imports the entire library (~70 KB), negating tree-shaking. Fix: use named imports. [src3]@date-fns/tz with TZDate; Day.js uses dayjs/plugin/timezone. Fix: test across DST boundaries and convert moment.tz() calls individually. [src3, src4]import { de } from 'date-fns/locale' or import 'dayjs/locale/de'. [src2]parseISO() strictly parses ISO 8601 only. new Date(str) gives inconsistent results for non-ISO strings. Fix: use parse() with explicit format for non-ISO strings. [src3]diff(other, 'days') (plural), Day.js uses diff(other, 'day') (singular). Fix: use singular unit names. [src4]# Check Moment.js version and bundle impact
npm ls moment moment-timezone
npx bundlephobia moment # ~290 KB minified, ~72 KB gzipped
# Find all Moment.js imports
grep -rn "require('moment')\|from 'moment'" --include='*.js' --include='*.ts' | wc -l
# Find format strings needing token conversion
grep -rn "\.format(" --include='*.js' --include='*.ts' | grep -v node_modules
# Check date-fns bundle size (tree-shaken: typically 3-15 KB)
npx bundlephobia date-fns
# Check Day.js bundle size (~2.9 KB gzipped core)
npx bundlephobia dayjs
# Verify no Moment.js references remain
grep -rn "moment" --include='*.js' --include='*.ts' --include='*.tsx' | grep -v node_modules | grep -v '.md'
# Check if Temporal API is available in your Node.js version
node -e "console.log(typeof Temporal !== 'undefined' ? 'Temporal available' : 'Temporal not available')"
| Library | Version | Status | Key Features | Node.js |
|---|---|---|---|---|
| Moment.js 2.x | Maintenance only | Legacy — no new features | Mutable API, bundled locales | Any |
| date-fns 4.1.0 | Current (Sep 2024) | Active | First-class TZ via @date-fns/tz, ESM-first, TZ in format/formatISO/formatRFC3339 | 18+ |
| date-fns 3.x | Previous (Dec 2023) | Maintained | TypeScript rewrite, ESM + CJS dual | 16+ |
| date-fns 2.x | Legacy | Bug fixes only | Original API, separate date-fns-tz | 12+ |
| Day.js 1.11.19 | Current | Active, stable (since 2018) | Moment-compatible API, plugin system, ESM support | 12+ |
| Temporal API | Stage 3 TC39 | Chrome 144 (Jan 2026), Firefox 139 (May 2025) | Native date/time, immutable, timezone-aware | Not yet stable |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Codebase uses Moment.js and needs bundle size reduction | Simple date formatting (1-2 operations) | Native Intl.DateTimeFormat |
| Starting a new project needing date manipulation | Application targets very old browsers without build tools | Moment.js (still works, just large) |
| Need immutable date operations to prevent bugs | Project is in maintenance mode with no new development | Leave Moment.js in place |
| Tree-shaking important for client-side performance | Need complete ISO 8601 / RFC 2822 parser | Luxon or Temporal (future) |
| Want functional style (choose date-fns) | Team prefers chainable object API | Choose Day.js instead |
| Want Moment-compatible API (choose Day.js) | Need advanced calendar systems (Hebrew, Islamic) | Luxon or Intl API |
| Targeting only Chrome 144+ / Firefox 139+ | Need cross-browser support including Safari | Temporal API (with polyfill) |
date-fns-tz package. The v4.1 release added timezone support to format, formatISO, formatISO9075, formatRelative, and formatRFC3339. If upgrading from v3, review the @date-fns/tz migration guide separately.@js-temporal/polyfill (~40 KB).