date-fns - Modular, tree-shakable date utility library for formatting, parsing, and manipulation
Recipe
npm install date-fnsimport { format, parseISO, addDays, differenceInDays } from "date-fns";
// Format a date
format(new Date(), "MMMM d, yyyy"); // "April 6, 2026"
// Parse ISO string
const date = parseISO("2026-04-06T12:00:00Z");
// Add days
const nextWeek = addDays(new Date(), 7);
// Difference between dates
const days = differenceInDays(new Date("2026-12-31"), new Date()); // days until end of yearWhen to reach for this: You need to format, parse, compare, or manipulate dates with a lightweight, tree-shakable library that uses native Date objects.
Working Example
// app/components/ActivityFeed.tsx
"use client";
import {
format,
formatDistanceToNow,
isToday,
isYesterday,
isThisWeek,
parseISO,
} from "date-fns";
interface Activity {
id: string;
action: string;
timestamp: string; // ISO string
user: string;
}
const ACTIVITIES: Activity[] = [
{ id: "1", action: "pushed to main", timestamp: new Date().toISOString(), user: "Alice" },
{ id: "2", action: "opened PR #42", timestamp: new Date(Date.now() - 3600000).toISOString(), user: "Bob" },
{ id: "3", action: "merged PR #41", timestamp: new Date(Date.now() - 86400000).toISOString(), user: "Carol" },
{ id: "4", action: "created issue", timestamp: new Date(Date.now() - 259200000).toISOString(), user: "Dave" },
{ id: "5", action: "deployed v2.1", timestamp: "2026-03-20T10:30:00Z", user: "Eve" },
];
function formatTimestamp(isoString: string): string {
const date = parseISO(isoString);
if (isToday(date)) {
return formatDistanceToNow(date, { addSuffix: true }); // "2 hours ago"
}
if (isYesterday(date)) {
return `Yesterday at ${format(date, "h:mm a")}`; // "Yesterday at 3:30 PM"
}
if (isThisWeek(date)) {
return format(date, "EEEE 'at' h:mm a"); // "Monday at 10:30 AM"
}
return format(date, "MMM d, yyyy"); // "Mar 20, 2026"
}
export default function ActivityFeed() {
return (
<div className="max-w-lg mx-auto p-6">
<h2 className="text-xl font-bold mb-4">Activity</h2>
<ul className="space-y-3">
{ACTIVITIES.map((activity) => (
<li key={activity.id} className="flex justify-between items-center border-b pb-2">
<div>
<span className="font-medium">{activity.user}</span>{" "}
<span className="text-gray-600">{activity.action}</span>
</div>
<time
dateTime={activity.timestamp}
className="text-sm text-gray-400 whitespace-nowrap"
>
{formatTimestamp(activity.timestamp)}
</time>
</li>
))}
</ul>
</div>
);
}What this demonstrates:
- Smart timestamp formatting with relative time for recent events
parseISOfor safe ISO string parsingisToday,isYesterday,isThisWeekfor conditional formattingformatDistanceToNowfor "X hours ago" style output- Semantic
<time>element withdateTimeattribute
Deep Dive
How It Works
- date-fns operates on native JavaScript
Dateobjects, not a custom wrapper class - Each function is a standalone ES module, enabling tree-shaking by bundlers (only imported functions are included in the bundle)
formatuses Unicode Technical Standard #35 tokens:yyyy(year),MM(month),dd(day),HH(24h),hh(12h),mm(minutes),ss(seconds)parseISOis preferred overnew Date(string)because it handles ISO 8601 strings consistently across browsers- Comparison functions (
isBefore,isAfter,isEqual) compare timestamps, not date strings - All functions are pure and do not mutate the input date
Variations
Date arithmetic:
import {
addDays,
addMonths,
addHours,
subWeeks,
startOfMonth,
endOfMonth,
eachDayOfInterval,
} from "date-fns";
const tomorrow = addDays(new Date(), 1);
const nextMonth = addMonths(new Date(), 1);
const twoHoursLater = addHours(new Date(), 2);
const lastWeek = subWeeks(new Date(), 1);
// Get all days in current month (useful for calendars)
const monthStart = startOfMonth(new Date());
const monthEnd = endOfMonth(new Date());
const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd });Locale support:
import { format, formatDistanceToNow } from "date-fns";
import { fr } from "date-fns/locale";
import { ja } from "date-fns/locale";
format(new Date(), "EEEE d MMMM yyyy", { locale: fr });
// "lundi 6 avril 2026"
formatDistanceToNow(new Date(Date.now() - 7200000), {
addSuffix: true,
locale: ja,
});
// "約2時間前"Date range and comparison:
import {
isWithinInterval,
isBefore,
isAfter,
differenceInCalendarDays,
differenceInBusinessDays,
areIntervalsOverlapping,
} from "date-fns";
const eventStart = new Date("2026-04-10");
const eventEnd = new Date("2026-04-15");
// Check if a date is in range
isWithinInterval(new Date("2026-04-12"), {
start: eventStart,
end: eventEnd,
}); // true
// Business days between dates (excludes weekends)
differenceInBusinessDays(eventEnd, eventStart); // 3
// Check for overlapping intervals
areIntervalsOverlapping(
{ start: eventStart, end: eventEnd },
{ start: new Date("2026-04-13"), end: new Date("2026-04-20") }
); // trueParsing custom date strings:
import { parse, isValid } from "date-fns";
const date = parse("04/06/2026", "MM/dd/yyyy", new Date());
console.log(isValid(date)); // true
const invalid = parse("not-a-date", "MM/dd/yyyy", new Date());
console.log(isValid(invalid)); // falseTypeScript Notes
- All date-fns functions accept
Dateornumber(timestamp) as date arguments - Return types are explicit:
formatreturnsstring,addDaysreturnsDate,differenceInDaysreturnsnumber - Locale types are exported from
date-fns/locale - No additional
@typespackage needed; types are included indate-fns
import type { Locale } from "date-fns";
import { fr } from "date-fns/locale";
function formatDate(date: Date | number, locale: Locale = fr): string {
return format(date, "PPP", { locale });
}Gotchas
-
Timezone confusion —
Dateobjects are always in the local timezone.formatoutputs local time, not UTC. Fix: Usedate-fns-tzfor timezone-aware formatting, or useformatInTimeZonefromdate-fns-tz. -
Format token case sensitivity —
MMis month,mmis minutes.DDis not valid (usedd). Fix: Refer to the format token table. Common tokens:yyyy-MM-dd HH:mm:ss. -
parseISO vs new Date —
new Date("2026-04-06")is parsed as UTC midnight, but displayed in local time, which can shift the date. Fix: Always useparseISOfor ISO strings to get consistent behavior. -
Mutable Date pitfall — While date-fns functions are pure, if you pass the same
Dateinstance to multiple places after mutating it elsewhere, you get unexpected results. Fix: Create newDateinstances or use the return values from date-fns functions. -
Bundle size with locales — Importing all locales adds significant bundle size. Fix: Import only the locales you need:
import { fr } from "date-fns/locale", notimport * as locales from "date-fns/locale". -
Server/client date mismatch — Server-rendered dates use the server's timezone, causing hydration mismatches. Fix: Format dates only on the client side, or pass pre-formatted strings from the server, or use
suppressHydrationWarningon the<time>element.
Alternatives
| Library | Best For | Trade-off |
|---|---|---|
| date-fns | Tree-shakable, functional API | No timezone support without date-fns-tz |
| dayjs | Moment.js drop-in replacement (2KB) | Plugin-based, mutable API |
| Temporal (proposal) | Future native JS date API | Not yet available in all runtimes |
| Luxon | Full timezone and i18n support | Larger bundle (around 20KB) |
| Moment.js | Legacy projects only | Deprecated, 67KB minified, mutable |
FAQs
Why should I use parseISO instead of new Date(string)?
new Date("2026-04-06")is parsed as UTC midnight but displayed in local time, which can shift the date by a dayparseISOhandles ISO 8601 strings consistently across all browsers- It returns an invalid date for non-ISO strings rather than guessing
How do I format a date as "2 hours ago" or "3 days ago"?
import { formatDistanceToNow } from "date-fns";
formatDistanceToNow(someDate, { addSuffix: true });
// "2 hours ago" or "in 3 days"The addSuffix option prepends "in" or appends "ago" automatically.
Gotcha: Why does MM give me months but mm gives me minutes?
- Format tokens are case-sensitive in date-fns (Unicode TR#35 standard)
MM= month (01-12),mm= minutes (00-59)DDis not valid -- useddfor day of month- Common safe pattern:
yyyy-MM-dd HH:mm:ss
How do I generate all days in a month for a calendar component?
import { startOfMonth, endOfMonth, eachDayOfInterval } from "date-fns";
const start = startOfMonth(new Date());
const end = endOfMonth(new Date());
const days = eachDayOfInterval({ start, end });How do I add locale support for non-English date formatting?
import { format } from "date-fns";
import { fr } from "date-fns/locale";
format(new Date(), "EEEE d MMMM yyyy", { locale: fr });
// "lundi 6 avril 2026"Import only the locales you need to keep bundle size small.
What is the difference between differenceInDays and differenceInCalendarDays?
differenceInDayscalculates based on full 24-hour periodsdifferenceInCalendarDayscounts calendar day boundaries crossed- Example: 11:00 PM to 1:00 AM is 0 with
differenceInDaysbut 1 withdifferenceInCalendarDays
How do I check if a date falls within a range?
import { isWithinInterval } from "date-fns";
isWithinInterval(new Date("2026-04-12"), {
start: new Date("2026-04-10"),
end: new Date("2026-04-15"),
}); // trueGotcha: Why do server-rendered dates cause hydration mismatches?
- The server formats dates in the server's timezone
- The client re-renders in the user's local timezone, producing different text
- Fix: format dates only on the client, pass pre-formatted strings from the server, or use
suppressHydrationWarningon the<time>element
How do I handle timezones with date-fns?
- date-fns itself has no timezone support --
Dateobjects use the local timezone - Install
date-fns-tzfor timezone-aware formatting - Use
formatInTimeZoneto display dates in a specific timezone - Always store and transmit dates as UTC ISO strings
How do I type a function that accepts both Date and timestamp number in TypeScript?
import type { Locale } from "date-fns";
import { format } from "date-fns";
function formatDate(
date: Date | number,
pattern: string = "PPP",
locale?: Locale
): string {
return format(date, pattern, { locale });
}All date-fns functions accept Date | number as date arguments.
How do I parse a custom date string format?
import { parse, isValid } from "date-fns";
const date = parse("04/06/2026", "MM/dd/yyyy", new Date());
console.log(isValid(date)); // trueAlways validate with isValid since parse returns an invalid Date for bad input.
Does date-fns tree-shake effectively and how do I verify?
- date-fns uses ES module exports so bundlers can eliminate unused functions
- Import individual functions:
import { format } from "date-fns"includes onlyformatand its dependencies - Avoid
import * as dateFns from "date-fns"which defeats tree-shaking - Check your bundle analyzer to confirm only imported functions are included
Related
- React Hooks — Custom hooks for date formatting
- Lodash — Utility functions that complement date-fns
- TanStack Query — Cache date-dependent API responses