Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
parseISO for safe ISO string parsingisToday, isYesterday, isThisWeek for conditional formattingformatDistanceToNow for "X hours ago" style output<time> element with dateTime attributeDate objects, not a custom wrapper classformat uses Unicode Technical Standard #35 tokens: yyyy (year), MM (month), dd (day), HH (24h), hh (12h), mm (minutes), ss (seconds)parseISO is preferred over new Date(string) because it handles ISO 8601 strings consistently across browsersisBefore, isAfter, isEqual) compare timestamps, not date stringsDate 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)); // falseDate or number (timestamp) as date argumentsformat returns string, addDays returns Date, differenceInDays returns numberdate-fns/locale@types package needed; types are included in date-fnsimport 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 });
}Timezone confusion - Date objects are always in the local timezone. format outputs local time, not UTC. Fix: Use date-fns-tz for timezone-aware formatting, or use formatInTimeZone from date-fns-tz.
Format token case sensitivity - MM is month, mm is minutes. DD is not valid (use dd). 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 use parseISO for ISO strings to get consistent behavior.
Mutable Date pitfall - While date-fns functions are pure, if you pass the same Date instance to multiple places after mutating it elsewhere, you get unexpected results. Fix: Create new Date instances 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", not import * 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 suppressHydrationWarning on the <time> element.
| 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 |
new Date("2026-04-06") is parsed as UTC midnight but displayed in local time, which can shift the date by a dayparseISO handles ISO 8601 strings consistently across all browsersimport { formatDistanceToNow } from "date-fns";
formatDistanceToNow(someDate, { addSuffix: true });
// "2 hours ago" or "in 3 days"The addSuffix option prepends "in" or appends "ago" automatically.
MM = month (01-12), mm = minutes (00-59)DD is not valid -- use dd for day of monthyyyy-MM-dd HH:mm:ssimport { startOfMonth, endOfMonth, eachDayOfInterval } from "date-fns";
const start = startOfMonth(new Date());
const end = endOfMonth(new Date());
const days = eachDayOfInterval({ start, end });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.
differenceInDays calculates based on full 24-hour periodsdifferenceInCalendarDays counts calendar day boundaries crosseddifferenceInDays but 1 with differenceInCalendarDaysimport { isWithinInterval } from "date-fns";
isWithinInterval(new Date("2026-04-12"), {
start: new Date("2026-04-10"),
end: new Date("2026-04-15"),
}); // truesuppressHydrationWarning on the <time> elementDate objects use the local timezonedate-fns-tz for timezone-aware formattingformatInTimeZone to display dates in a specific timezoneimport 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.
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.
import { format } from "date-fns" includes only format and its dependenciesimport * as dateFns from "date-fns" which defeats tree-shakingReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥