/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/ checks the ISO 8601 calendar date format: a four-digit year, a month from 01 to 12 and a day from 01 to 31, each zero-padded and joined by hyphens.
What it accepts
The tester below checks one date per line with g and m; for a single value, drop both flags.
2026-10-14 matches
1999-12-31 matches
2024-02-29 matches: 2024 is a leap year
2026-13-01 no: month 13
2026-00-10 no: month 00
2026-1-5 no: not zero-padded
26-10-14 no: two-digit year
2026-02-31 matches, though February has no 31st
The part a regex can’t do
The day range doesn’t depend on the month, so 2026-02-31 and 2026-04-31 pass. A regex that knows month lengths and leap years exists, but it runs to a couple of hundred characters, and a calendar is easier to ask than to spell out.
new Date() isn’t that calendar on its own. In Chrome and Node, new Date('2026-02-31') doesn’t fail: it rolls over to March 3. Parse the date, write it back out and compare:
const ISO_DATE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
function isRealDate(text) {
if (!ISO_DATE.test(text)) return false;
const date = new Date(`${text}T00:00:00Z`);
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === text;
}
isRealDate('2024-02-29'); // true
isRealDate('2026-02-29'); // false: it came back as 2026-03-01
The T00:00:00Z pins the date to midnight UTC, so the visitor’s timezone can’t move it to another day.
Dates with a time
For a full timestamp such as 2026-10-14T09:30:00Z or 2026-10-14T09:30:00.125+02:00, as RFC 3339 and JSON APIs write them, extend the pattern:
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$
That one checks only the shape of each field; run the same round-trip test on the date part before you trust it.