Détection moderne des fonctionnalités sur iOS (sans la douleur)
La détection de fonctionnalités iOS devient simple: CSS.supports, tests comportementaux et sniffing UA limité pour iOS 26 et anciens iPad.
Détectez les fonctionnalités par défaut : testez la capacité dont vous avez réellement besoin avec 'IntersectionObserver' in window ou CSS.supports('property', 'value'), et réservez les vérifications de user-agent à la poignée de cas iOS qui ne peuvent véritablement pas être détectés par capacité.
Quiconque a maintenu une table de numéros de version iOS dans une base de code connaît le scénario : Apple publie une mise à jour, la table devient obsolète, et quelque chose casse en production avant que quiconque n’ouvre un ticket. Les vérifications de capacités court-circuitent complètement ce cycle.
La détection de fonctionnalités résout l’essentiel des difficultés liées à Safari et iOS : vous cessez de maintenir des tables de versions fragiles et commencez à demander au navigateur ce dont il est capable. Le plus difficile, c’est le résidu : un petit ensemble de cas limites iOS (les anciens iPad qui se déclarent comme un macOS de bureau, le gel du user-agent sous iOS 26) où aucune vérification de capacité n’existe et où vous devez recourir au sniffing avec prudence. Cet article vous donne le playbook moderne pour les deux situations.
Points clés à retenir
- Détectez les capacités directement avec
'x' in window,CSS.supports()et le chaînage optionnel. MDN qualifie cette approche de « stratégie beaucoup plus fiable » que l’analyse de la chaîne user-agent. - La présence n’est pas une preuve : Safari peut retourner
truevia@supportspour une fonctionnalité qu’il n’applique pas réellement ; pour les « menteurs » connus, affichez donc l’élément hors écran et mesurez-le avecgetBoundingClientRect(). - Puisque tous les navigateurs sur iOS reposent sur WebKit et que la version de Safari suit la version majeure du système, une vérification
isMobileWebKit()associée à un garde-fouCSS.supports()permet de déduire la version d’iOS sans aucune analyse de UA. - Sur iOS 26+, Safari gèle le token de version du système dans son UA sur une valeur antérieure à 26, valeur qui a elle-même évolué (
18_6→18_6_2→18_7). Ne la codez jamais en dur ; analysez plutôt le tokenVersion/. - Ce gel est un comportement propre à Safari, documenté pour iOS et iPadOS 26 ; Chrome et Firefox sur iOS rapportent toujours la véritable version d’iOS.
La détection de fonctionnalités est la règle par défaut
Testez la capacité, pas le navigateur. Une vérification de fonctionnalité s’adapte automatiquement lorsque Apple publie une mise à jour, ne nécessite aucune table de maintenance et fonctionne à l’identique sur tous les moteurs — c’est précisément pour cela qu’Apple et MDN la recommandent plutôt que le sniffing de user-agent. Vous disposez de trois outils pour cette tâche.
Pour les API JavaScript, sondez l’objet global ou utilisez le chaînage optionnel :
if ("IntersectionObserver" in window) {
// wire up lazy-loading
}
navigator.share?.({ title: "Modern feature detection" });
Pour le CSS, utilisez CSS.supports() en JS ou la règle-@ @supports dans votre feuille de style :
@supports (text-wrap-style: stable) {
h1 { text-wrap-style: balance; }
}
Notez que navigator.userAgentData n’est pas une solution de repli ici : cette API est spécifique à Chromium et marquée comme expérimentale, si bien que Safari et Firefox ne l’implémentent pas. Elle ne remplace jamais la détection de fonctionnalités lorsque votre cible est iOS Safari.
Quand la détection de fonctionnalités ment : la présence n’est pas une preuve
Discover how at OpenReplay.com.
La détection de fonctionnalités présente deux modes de défaillance qu’il vaut la peine de nommer, car le remède diffère. Le premier est un anti-pattern : détecter une fonctionnalité B sans lien pour en déduire la fonctionnalité A. Comme le documente A Beautiful Site, dès l’instant où le navigateur livre une fonctionnalité avant l’autre, votre vérification casse silencieusement. Ne couplez pas vos vérifications à des indicateurs indirects.
Le second est plus subtil : la présence n’est pas une preuve. Safari peut retourner true via @supports pour une valeur qu’il n’applique pas réellement, comme Evil Martians l’a constaté avec le mot-clé d’alignement CSS safe. Pour ces « menteurs » connus, ne faites pas confiance au drapeau de support. Affichez l’élément hors écran et mesurez le résultat réel avec getBoundingClientRect() :
const supportsSafeAlign = () => {
const box = document.createElement("div");
const child = document.createElement("span");
child.textContent = "measure me";
Object.assign(box.style, {
display: "flex",
justifyContent: "safe center",
width: "5%",
position: "absolute",
top: "-9999px",
left: "-9999px",
});
box.appendChild(child);
document.body.appendChild(box);
const applied = child.getBoundingClientRect().left >= box.getBoundingClientRect().left;
document.body.removeChild(box);
return applied;
};
Un test comportemental n’a besoin d’aucune affirmation de version pour être juste : il observe ce qui a réellement été rendu. C’est précisément la catégorie de bug que le session replay met bien en évidence : une fonctionnalité conditionnée qui passe sa vérification de support dans votre environnement de test, mais qui se comporte mal en silence sur la build iOS d’un vrai utilisateur.
Détecter les versions d’iOS par détection de fonctionnalités
Puisque tous les navigateurs sur iOS reposent sur WebKit et que la version de Safari est liée à la version majeure du système, une vérification isMobileWebKit() associée à un garde-fou CSS.supports() portant sur une propriété introduite dans une version connue de Safari vous permet de déduire la version d’iOS sans analyser la moindre chaîne user-agent. L’heuristique « WebKit mobile » s’appuie sur un événement de geste exposé par WebKit :
const isMobileWebKit = () => "ongesturechange" in window;
Pour le garde-fou de version, recherchez la première version prenant en charge la propriété dans les notes de version de Safari publiées par Apple ou dans les données de compatibilité de MDN, puis testez-la. La propriété abrégée text-wrap-style est arrivée dans Safari 17.5, qui a livré ses valeurs balance, stable et auto simultanément ; n’importe laquelle de ces valeurs constitue donc un garde-fou propre pour iOS 17.5+ :
const isAtLeastIOS175 = () =>
window.CSS?.supports("text-wrap-style", "stable") ?? false;
Vérifiez vous-même la correspondance propriété ↔ version avant la mise en production. Les notes de version omettent parfois certains changements, et un drapeau « supporté » peut mentir (voir la section précédente). Considérez isMobileWebKit() comme une heuristique solide, non comme une garantie de spécification : Apple autorise d’autres moteurs de navigation dans l’UE sur iOS 17.4+, donc « iOS signifie WebKit » est très largement vrai, mais pas absolu.
Quand le sniffing de user-agent devient l’ultime recours, strictement circonscrit
Ne recourez au user-agent que lorsque deux conditions sont réunies simultanément. La capacité n’a véritablement aucun test de fonctionnalité et une mauvaise supposition ne vous coûte rien de plus grave qu’un défaut cosmétique. Deux cas iOS remplissent ce critère.
Les anciens iPad. La détection de fonctionnalités ne peut pas distinguer un iPad d’un Mac, car depuis iPadOS 13, le user-agent par défaut d’un iPad est la même chaîne que celle envoyée par un Mac. Combinez plutôt les signaux. Un user-agent qui se présente comme Safari sur macOS de bureau, associé à une vérification WebKit mobile positive et à un nombre de points de contact non nul, indique un iPad déguisé en Mac. Le test des points de contact a son importance : les Mac rapportent navigator.maxTouchPoints à 0, ce qui empêche un véritable Mac de correspondre même si le signal d’événement de geste est présent sur Safari de bureau.
const looksLikeMacSafari = /Macintosh/.test(navigator.userAgent);
const isIPad = () =>
looksLikeMacSafari && isMobileWebKit() && navigator.maxTouchPoints > 0;
Le gel du UA sous iOS 26. Sur iOS 26 et iPadOS 26, Safari a cessé d’inclure la version du système en cours d’exécution dans son user-agent et a figé le token sur une version antérieure. Cette valeur figée a elle-même évolué au fil des versions intermédiaires (18_6 au lancement, puis 18_6_2 dans Safari 26.1, puis 18_7 à partir d’iOS 26.2) — raison précise pour laquelle vous ne devez jamais la coder en dur. Analysez le token Version/, qui reflète toujours la version majeure réelle de Safari (et donc d’iOS) :
const iosMajor = () => {
const m = navigator.userAgent.match(/Version\/(\d+)/);
return m ? Number(m[1]) : null; // 26 on iOS 26.x Safari
};
Deux précisions affinent ce point. Premièrement, le gel concerne uniquement Safari : des analyses indépendantes de logs serveur réalisées par AppleInsider et Lapcat Software confirment toutes deux que Chrome et Firefox sur iOS rapportent toujours la véritable version du système. « On ne peut pas détecter iOS 26 » est donc un problème spécifique à Safari, et non un problème à l’échelle d’iOS. Deuxièmement, si vous préférez ne pas prendre en charge l’analyse vous-même, ua-parser-js est en 2.0.10 dans sa dernière version npm : épinglez donc 2.0.10+. Ce paquet possède également un historique documenté d’incidents de chaîne d’approvisionnement, vérifiez donc ce que vous installez.
Le playbook
Le flux complet est court, et il s’exécute dans l’ordre :
- Détectez d’abord les fonctionnalités.
'x' in window,CSS.supports(),@supports, chaînage optionnel. Cela couvre l’écrasante majorité des décisions de conditionnement. - Testez le comportement des menteurs. Lorsqu’un drapeau de support n’est pas fiable, effectuez le rendu hors écran et mesurez avec
getBoundingClientRect(). - Déduisez la version d’iOS sans le UA.
isMobileWebKit()plus un garde-fouCSS.supports()sur une propriété confirmée par les notes de version. - Ne sniffez que les cas indétectables. Le combinateur pour les anciens iPad et le token
Version/pour le gel d’iOS 26. Ne les utilisez que là où un sniffing raté ne fait perdre aucune fonctionnalité. - Testez sur de vrais appareils et sur simulateurs. Les notes de version comme les drapeaux de support laissent des angles morts, et seul du matériel réel tranche la question.
La détection de fonctionnalités est la règle par défaut parce qu’elle survit aux mises à jour que vous n’avez pas anticipées ; le sniffing de user-agent est l’exception circonscrite aux deux ou trois cas iOS que la plateforme rend véritablement indétectables. Enchaînez les vérifications dans cet ordre, appuyez-vous sur l’analyse du token Version/ plutôt que sur une quelconque chaîne de version système figée, et confirmez le résultat sur un véritable ancien iPad avant la mise en production.
FAQ
Peut-on détecter iOS 26 en JavaScript malgré le gel du user-agent ?
Oui. À partir d'iOS 26, Safari gèle le token de version du système dans son user-agent sur une valeur antérieure à 26, valeur qui a évolué au fil des versions intermédiaires (18_6, puis 18_6_2, puis 18_7 à partir d'iOS 26.2) : la coder en dur échoue donc. Analysez plutôt le token Version/, qui rapporte toujours la version majeure réelle de Safari et, par conséquent, la version majeure d'iOS. Sur Safari sous iOS 26.x, une correspondance avec Version/(\\d+) renvoie 26.
Le gel du user-agent sous iOS 26 affecte-t-il Chrome et Firefox sur iOS ?
Non. Apple documente le token de version système figé comme un comportement propre à Safari sur iOS et iPadOS 26. Des analyses indépendantes de logs serveur menées par AppleInsider et Lapcat Software confirment toutes deux que Chrome et Firefox sur iOS rapportent encore la véritable version du système dans leurs chaînes user-agent, même si tous les navigateurs iOS reposent sur WebKit. « On ne peut pas détecter iOS 26 » est donc un problème spécifique à Safari, et non un problème à l'échelle d'iOS.
Comment distinguer un iPad d'un Mac dans le navigateur ?
Combinez deux signaux, car la détection de fonctionnalités seule ne peut pas les distinguer : depuis iPadOS 13, le user-agent par défaut d'un iPad est la même chaîne que celle envoyée par un Mac. Si le user-agent ressemble à Safari sur macOS de bureau mais qu'une vérification WebKit mobile indique que vous êtes sur un appareil tactile, vous avez affaire à un iPad qui se fait passer pour un Mac. Comme les événements de geste apparaissent aussi sur Safari de bureau, conditionnez sur navigator.maxTouchPoints supérieur à 0, puisque les Mac rapportent 0.
navigator.userAgentData fonctionne-t-il dans Safari ?
Non. navigator.userAgentData est spécifique à Chromium et marquée comme expérimentale sur MDN : Safari et Firefox ne l'implémentent donc pas. Ce n'est jamais un substitut viable à la détection de fonctionnalités lorsque votre cible est iOS Safari. Testez plutôt directement les capacités avec 'x' in window ou CSS.supports(), une approche que MDN juge bien plus fiable que la lecture du user-agent.