Pour valider correctement un numéro de téléphone en France, il faut faire plus que “tester une suite de chiffres”. Il faut décider quels formats tu acceptes, à quel moment tu normalises la saisie, et où tu fais la vraie validation métier. Cet article te donne des motifs utiles, des exemples concrets en JavaScript et en HTML, et les pièges que je vois revenir le plus souvent en production.
Ce qu’il faut garder en tête avant d’écrire le motif
- Un numéro français courant se présente souvent en 10 chiffres au format national, ou en +33 au format international.
- Pour une saisie utilisateur, il vaut mieux accepter quelques séparateurs simples comme l’espace, le point ou le tiret.
- Pour le stockage et les échanges entre systèmes, le format E.164 est généralement le plus propre.
- Une regex sert très bien à filtrer un format, mais elle ne prouve pas qu’un numéro existe réellement.
- Dans un formulaire, le navigateur aide, mais il ne remplace pas une validation côté serveur.
- Les numéros courts, les urgences et certains services doivent souvent être traités à part.
Ce que ton motif doit accepter ou refuser
Quand on travaille sur une expression régulière pour les numéros de téléphone français, la première erreur consiste à croire qu’il existe un seul format “officiel” à accepter. En pratique, je distingue toujours deux besoins différents : la saisie humaine et la donnée canonique. Le premier doit être tolérant, le second doit être strict.
Pour la France, l’ARCEP distingue plusieurs grandes familles de numéros. Les blocs 01 à 05 couvrent les fixes géographiques, 06 et 07 les mobiles, tandis que 08 et 09 servent à d’autres usages. Si ton application ne gère que des contacts classiques, tu n’as pas besoin d’englober tous les cas possibles dans une seule regex géante.
| Format | Exemple | À accepter ? | Remarque |
|---|---|---|---|
| National avec espaces | 01 23 45 67 89 | Oui | C’est le format le plus lisible pour un utilisateur français. |
| National sans séparateur | 0123456789 | Oui | Très courant dans les formulaires et les exports. |
| International | +33 1 23 45 67 89 | Oui | Pratique pour l’échange entre systèmes et le stockage normalisé. |
| Avec tirets ou points | 01-23-45-67-89 | Selon le contexte | Utile si tu veux être souple sur la présentation saisie par l’utilisateur. |
| Numéros courts | 112, 15, 17, 18 | Non dans la même règle | Mieux vaut les traiter à part, car ils ne suivent pas la même logique. |
La bonne question n’est donc pas “quelle regex est la plus complète”, mais “quels cas je veux réellement accepter”. Une fois ce cadre posé, on peut passer à des motifs concrets, beaucoup plus simples à maintenir.
Des motifs prêts à l’emploi selon ton cas
Je préfère toujours partir d’un besoin précis plutôt que d’une formule universelle. Un motif trop ambitieux finit vite par rejeter des numéros corrects, ou à l’inverse par accepter des chaînes qui n’ont rien à voir avec un téléphone.
Validation souple pour une saisie utilisateur
Ce premier motif accepte un numéro français au format national ou international, avec quelques séparateurs courants :
^(?:0|\+33 ?)[1-9](?:[ .-]?\d{2}){4}$Il accepte par exemple 01 23 45 67 89, 0123456789, 06-12-34-56-78 et +33 6 12 34 56 78. Il refuse en revanche +33 06 12 34 56 78, ce qui est volontaire : après +33, le zéro initial ne doit pas rester.
Stockage canonique en E.164
Si tu veux une donnée propre en base, je recommande de stocker le numéro sous une forme canonique, sans séparateurs :
^\+33[1-9]\d{8}$Cette version est plus stricte, mais elle est aussi plus fiable pour la recherche, la déduplication et les échanges entre services. En pratique, j’utilise souvent une regex souple à l’entrée, puis je convertis vers ce format normalisé avant l’enregistrement.
Lire aussi : Polymorphisme en POO - Comprendre pour mieux coder
Extraction dans un texte
Quand tu dois repérer un numéro au milieu d’un paragraphe, le problème change un peu. Il ne s’agit plus seulement de valider une chaîne entière, mais d’identifier une occurrence plausible sans capturer n’importe quel bloc de chiffres :
(?<!\d)(?:0|\+33 ?)[1-9](?:[ .-]?\d{2}){4}(?!\d)Cette version s’appuie sur des bornes autour du motif pour éviter de “mordre” dans un identifiant plus long. Si ton environnement ne gère pas les lookbehind, il faut adapter la stratégie, par exemple avec un groupe capturant et un nettoyage après coup.
Le point important ici, c’est que chaque motif répond à un usage différent. Une regex de validation formulaire ne doit pas forcément être la même qu’une regex d’extraction ou qu’un contrôle de stockage.
L’intégrer dans un formulaire HTML sans créer de faux rejets
En front-end, je vois souvent deux confusions. La première consiste à penser que type="tel" valide automatiquement le format. La seconde consiste à mettre toute la logique dans l’attribut pattern et à oublier le serveur. MDN rappelle d’ailleurs que le champ téléphone n’impose pas un format unique, ce qui est logique vu la diversité des numéros dans le monde.
Ici, inputmode="tel" aide surtout l’interface mobile à afficher le bon clavier, tandis que pattern sert à la validation côté navigateur. Le détail que beaucoup oublient, c’est que le navigateur ne fait qu’un contrôle client. Si tu veux un système robuste, la même règle doit être rejouée côté serveur.
Je conseille aussi de rester simple dans le message d’erreur. Un texte du type “Saisis un numéro français au format 01 23 45 67 89 ou +33 1 23 45 67 89” est plus utile qu’un message technique qui expose la regex elle-même. Le but n’est pas d’impressionner l’utilisateur, mais de le faire réussir du premier coup.
Une fois le formulaire maîtrisé, la vraie différence se joue au moment où tu stockes et compares les valeurs.
Normaliser avant de stocker ou comparer
Si je ne devais garder qu’une habitude, ce serait celle-ci : valider l’entrée, puis normaliser la donnée. Un numéro affiché dans un formulaire n’est pas forcément le format que tu veux conserver en base. Pour la plupart des projets, je préfère une forme canonique unique, parce qu’elle évite les doublons et simplifie les recherches.
function normalizeFrPhone(value) {
const compact = value.replace(/[\s().-]/g, '');
if (compact.startsWith('0')) {
return `+33${compact.slice(1)}`;
}
return compact;
}Avec cette approche, 01 23 45 67 89 devient +33123456789. C’est beaucoup plus pratique si tu dois ensuite comparer deux enregistrements, envoyer un SMS via une API, ou afficher les données dans plusieurs pays. Le format E.164 reste une valeur de référence simple à manipuler.
Attention toutefois : cette fonction normalise, elle ne valide pas tout. Elle ne sait pas si le numéro appartient à un vrai utilisateur, ni s’il s’agit d’un fixe, d’un mobile, d’un service ou d’un numéro court. Cette responsabilité doit rester séparée, autrement tu mélanges des règles techniques et des règles métier.
Les pièges qui font perdre du temps
-
Oublier les ancres : sans
^et$, tu risques d’accepter des morceaux de texte au lieu d’un numéro complet. - Autoriser trop de chiffres : un motif trop permissif finit par valider des chaînes longues ou des identifiants qui n’ont rien à voir avec un téléphone.
- Confondre affichage et stockage : le format visible par l’utilisateur n’est pas forcément le format que tu dois enregistrer.
-
Ignorer le
+33: beaucoup d’équipes valident uniquement le format national, puis cassent les saisies internationales. -
Accepter
(0)partout : ce zéro entre parenthèses est souvent un artefact d’affichage, pas une vraie donnée à conserver telle quelle. - Vouloir tout couvrir dans une seule regex : les numéros courts, les extensions internes et les services spéciaux méritent souvent un traitement séparé.
Le dernier point est le plus important pour moi. Une regex n’est pas un moteur de compréhension métier. Si ton application doit gérer des cas avancés, comme des numéros de services, des extensions de poste ou des formulaires internationaux, il vaut mieux découper les règles plutôt que d’empiler des alternatives difficiles à maintenir.
Le réglage qui marche le mieux dans un projet réel
Dans un projet web classique en France, je pars presque toujours sur la même logique : une validation souple à l’entrée, une normalisation vers E.164 au stockage, et une validation métier séparée pour tout ce qui sort du cas standard. Cette approche est un peu moins spectaculaire qu’une regex “ultime”, mais elle tient mieux dans le temps.
Autrement dit, la bonne réponse n’est pas seulement de savoir écrire un motif. C’est surtout de savoir à quel moment il doit intervenir et ce qu’il a le droit de décider. Si tu respectes cette séparation, tu obtiens un système plus lisible, plus fiable et bien plus simple à faire évoluer quand les besoins changent.