
The regex below validates a French DD/MM/YYYY date (separator /, - or .) and rejects impossible dates such as 30 February or 31 April, leap years included. For the ISO YYYY-MM-DD format, a short regex is enough if you then confirm with the Date object in JavaScript or checkdate() in PHP.
Validating a date typed by hand is trickier than it looks: “30/02/2026” has the right shape and does not exist. The date regex below checks the DD/MM/YYYY format, with /, - or . as the separator, and rejects impossible days, including 29 February in non-leap years. You will then find the JavaScript, PHP and HTML versions, the ISO variant, and the case where the Date object does better than a regex.
The date regex, ready to copy
^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$It is long because it reasons like a calendar. Three branches, separated by |:
- The 31st only exists in 31-day months (
01, 03, 05, 07, 08, 10, 12), and the 29th and 30th in every month except February. - 29 February is only accepted if the year is a leap year: divisible by 4 (
0[48]|[2468][048]|[13579][26]), or a century divisible by 400 ((?:16|[2468][048]|[3579][26])00). 2000 and 2024 pass, 1900 and 2023 are rejected. - From the 1st to the 28th, every month accepts the date.
The back-references \1, \2, \3 and \4 require the second separator to be identical to the first: 07/09/2026 and 07-09-2026 pass, 07/09-2026 does not.
To accept the slash only, replace each (\/|-|\.) with (\/). The parentheses are still needed: the back-references \1 to \4 point to those groups.
The dates tested
Actual result of the regex under Node.js 22 and PHP 8.5, identical in both engines:
| Input | Result | Reason |
|---|---|---|
31/12/2026 | accepted | 31 in a 31-day month |
29/02/2024 | accepted | 2024 is a leap year |
29/02/2000 | accepted | 2000 is divisible by 400 |
01/01/2026 and 1/1/2026 | accepted | the leading zero is optional |
07-09-2026 and 07.09.2026 | accepted | other permitted separators |
29/02/2023 | rejected | 2023 is not a leap year |
29/02/1900 | rejected | century not divisible by 400 |
30/02/2026 | rejected | February never has 30 days |
31/04/2026 | rejected | April has 30 days |
00/01/2026 | rejected | day 0 does not exist |
12/13/2026 | rejected | month 13: the regex really is DD/MM, not MM/DD |
07/09-2026 | rejected | mismatched separators |
2026-09-07 | rejected | ISO format, see below |
07/09/26 and 31/12/1899 | accepted | two-digit year, or a year before 1900: bound it in your code if need be |
JavaScript version
The function anchors the regex with ^ and $, strips the whitespace around the input and returns a boolean.
/**
* Checks a date in the DD/MM/YYYY, DD-MM-YYYY or DD.MM.YYYY format.
* @param {string} date
* @returns {boolean}
*/
function validateDate(date) {
const reg = /^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$/;
return reg.test(String(date).trim());
}
console.log(validateDate('29/02/2024')); // true
console.log(validateDate('29/02/2023')); // false
console.log(validateDate('31/04/2026')); // falseIn a form, the pattern is the same as for validating an email address: listen for submit, call preventDefault() if the date is invalid, and show the message next to the field.
PHP version
The server-side check is mandatory: JavaScript can be disabled. The same pattern works with preg_match(); the back-references are written \\1 in a single-quoted PHP string.
/**
* Checks a date in the DD/MM/YYYY, DD-MM-YYYY or DD.MM.YYYY format.
*/
function isValidDate(string $date): bool
{
$pattern = '/^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$/';
return preg_match($pattern, trim($date)) === 1;
}
var_dump(isValidDate('29/02/2024')); // bool(true)
var_dump(isValidDate('30/02/2026')); // bool(false)The regex-free alternative: checkdate()
PHP already knows whether a day, month and year triplet exists. A small regex to split the input, then checkdate() to judge it: shorter and more readable.
function isValidDateFr(string $date): bool
{
if (!preg_match('#^(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})$#', trim($date), $m)) {
return false;
}
return checkdate((int) $m[2], (int) $m[1], (int) $m[3]); // month, day, year
}
var_dump(isValidDateFr('29/02/2024')); // bool(true)
var_dump(isValidDateFr('29/02/2023')); // bool(false)
var_dump(isValidDateFr('31/04/2026')); // bool(false)Beware of DateTime::createFromFormat('d/m/Y', '29/02/2023'): it does not return false but 1st March 2023, reporting the discrepancy in DateTime::getLastErrors(). For a strict check, checkdate() is safer.
HTML version
If the user types the date as free text, the pattern attribute applies the regex before submission. It is implicitly anchored: ^ and $ are removed.
<label for="naissance">Date de naissance (JJ/MM/AAAA)</label>
<input
id="naissance"
name="naissance"
type="text"
inputmode="numeric"
placeholder="JJ/MM/AAAA"
pattern="(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})|(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))|(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})"
title="Une date valide au format JJ/MM/AAAA"
required
>But the best HTML solution is not the regex: it is type="date". The browser displays a picker suited to the user’s language, prevents 30 February, and always sends the value in the ISO YYYY-MM-DD format, whatever it displays.
<input id="naissance" name="naissance" type="date" min="1900-01-01" max="2026-12-31" required>The consequence on the server: with type="date", it is the ISO format you have to validate, not the French one.
ISO variant: YYYY-MM-DD
For an ISO date, a short regex checks the shape and the Date object confirms that the day exists. Together, the two replace the big regex.
function isValidIsoDate(value) {
const m = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/.exec(value);
if (!m) return false;
const [, y, mo, d] = m.map(Number);
const date = new Date(y, mo - 1, d);
// new Date(2026, 1, 30) returns 2 March: we check that nothing has shifted
return date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d;
}
console.log(isValidIsoDate('2026-09-07')); // true
console.log(isValidIsoDate('2026-02-30')); // false
console.log(isValidIsoDate('2026-13-01')); // falseThe same technique applies to the French format: split with /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/, then check with Date. It is the JavaScript equivalent of PHP’s checkdate().
What about the American MM/DD/YYYY format?
This article’s regex is French: day first, then month. For an American audience, swap the first two blocks or, simpler still, normalise everything to ISO as soon as it is entered, with type="date". An application that accepts both orders without telling them apart gets it wrong half the time for every date from the 1st to the 12th.
Comparing two dates after validation
Once the date has been validated and converted into a Date object, comparisons are straightforward: the < and > operators compare instants, and subtraction gives a difference in milliseconds.
const debut = new Date('2026-09-07');
const fin = new Date('2026-12-25');
console.log(debut < fin); // true
console.log(Math.round((fin - debut) / 86_400_000)); // 109 daysFormatting for display (toLocaleDateString, Intl.DateTimeFormat) is another subject, as are date and time in PHP and Carbon in Laravel.
Regex or Date object: which to choose?
- The big regex when you want a single rule, identical in JavaScript, PHP and HTML, with no dependency on a library or on a
Dateobject. - Split +
Dateorcheckdate()when readability matters, or when you then need to work with the date: you already have it as an object. type="date"for any modern form: input is guided, the value is normalised, and all that is left is the ISO check on the server.
The other validations in the series: email address, IP address, postcode and password.
Common errors
new Date(2026, 1, 30) does not throw: it returns 2 March 2026. Always compare year, month and day after construction.2026-09-07 is rejected by the DD/MM/YYYY regex. Use this article's ISO variant or, better, input type="date", which already returns that format.type="date" field displays “07/09/2026” in French but the value it sends is “2026-09-07”. Server-side validation has to expect the ISO format.

