Detección moderna de características en iOS (sin sufrimiento)
La detección de funciones en iOS sin dolor: usa CSS.supports, pruebas de comportamiento y un UA mínimo para iOS 26 y iPads antiguos.
Detecta características por defecto: comprueba la capacidad que realmente necesitas con 'IntersectionObserver' in window o CSS.supports('property', 'value'), y reserva las comprobaciones de user-agent para el puñado de casos de iOS que genuinamente no pueden detectarse por características.
Cualquiera que haya mantenido una tabla de números de versión de iOS en una base de código conoce la rutina: Apple publica una actualización, la tabla queda obsoleta y algo se rompe en producción antes de que nadie abra un ticket. Las comprobaciones de capacidades se saltan ese ciclo por completo.
La detección de características resuelve la mayoría de los dolores de cabeza con Safari e iOS: dejas de mantener tablas de versiones frágiles y empiezas a preguntarle al navegador qué puede hacer. La parte difícil es el residuo: un pequeño conjunto de casos límite de iOS (iPads antiguos que se identifican como macOS de escritorio, la congelación del user-agent en iOS 26) donde no existe ninguna comprobación de capacidad y hay que hacer sniffing con cuidado. Este artículo te ofrece el manual moderno para ambos escenarios.
Puntos clave
- Detecta capacidades directamente con
'x' in window,CSS.supports()y el encadenamiento opcional. MDN lo califica como “una estrategia mucho más fiable” que analizar la cadena del user-agent. - La presencia no es prueba: Safari puede informar
@supportscomo verdadero para una característica que en realidad no aplica, así que para los “mentirosos” conocidos renderiza el elemento fuera de pantalla y mídelo congetBoundingClientRect(). - Como todos los navegadores en iOS se ejecutan sobre WebKit y la versión de Safari sigue la versión mayor del sistema operativo, una comprobación
isMobileWebKit()junto con una barrera deCSS.supports()permite inferir la versión de iOS sin analizar el UA en absoluto. - En iOS 26+, Safari congela el token del sistema operativo en su UA a un valor anterior a la 26 que a su vez ha ido cambiando (
18_6→18_6_2→18_7). Nunca lo codifiques de forma fija; analiza en su lugar el tokenVersion/. - La congelación es un comportamiento propio de Safari, documentado para iOS y iPadOS 26; Chrome y Firefox en iOS siguen informando la versión real de iOS.
La detección de características es la opción por defecto
Comprueba la capacidad, no el navegador. Una comprobación de característica se adapta automáticamente cuando Apple publica una actualización, no requiere ninguna tabla de mantenimiento y funciona de forma idéntica en todos los motores, que es exactamente el motivo por el que Apple y MDN la recomiendan frente al sniffing de user-agent. Dispones de tres herramientas para el trabajo.
Para las APIs de JavaScript, sondea el objeto global o usa el encadenamiento opcional:
if ("IntersectionObserver" in window) {
// wire up lazy-loading
}
navigator.share?.({ title: "Modern feature detection" });
Para CSS, usa CSS.supports() en JS o la regla @supports en tu hoja de estilos:
@supports (text-wrap-style: stable) {
h1 { text-wrap-style: balance; }
}
Ten en cuenta que navigator.userAgentData no es una alternativa aquí: es exclusivo de Chromium y está marcado como experimental, así que Safari y Firefox no lo implementan. Nunca sustituye a la detección de características cuando tu objetivo es Safari en iOS.
Cuando la detección de características miente: la presencia no es prueba
Discover how at OpenReplay.com.
La detección de características tiene dos modos de fallo que conviene nombrar, porque la solución difiere. El primero es un antipatrón: detectar una característica B no relacionada para inferir la característica A. Como documenta A Beautiful Site, en el momento en que el navegador publica una característica antes que la otra, tu comprobación se rompe silenciosamente. No acoples las comprobaciones a indicadores indirectos.
El segundo es más sutil: la presencia no es prueba. Safari puede informar @supports como true para un valor que en realidad no aplica, como descubrió Evil Martians con la palabra clave de alineación safe de CSS. Para esos “mentirosos” conocidos, no confíes en el indicador de soporte. Renderiza el elemento fuera de pantalla y mide el resultado real con 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;
};
Una prueba de comportamiento no necesita ninguna afirmación de versión para ser correcta: observa lo que realmente se renderizó. Esta es la clase de error que el session replay saca a la luz especialmente bien: una característica condicionada que supera su comprobación de soporte en tu entorno de pruebas pero se comporta mal de forma silenciosa en la compilación de iOS de un usuario real.
Detectar versiones de iOS mediante detección de características
Como todos los navegadores en iOS se ejecutan sobre WebKit y la versión de Safari está vinculada a la versión mayor del sistema operativo, una comprobación isMobileWebKit() junto con una barrera de CSS.supports() para una propiedad introducida en una versión conocida de Safari te permite inferir la versión de iOS sin analizar en absoluto una cadena de user-agent. La heurística de WebKit móvil se basa en un evento de gestos que WebKit expone:
const isMobileWebKit = () => "ongesturechange" in window;
Para la barrera de versión, busca la primera versión que admite una propiedad en las notas de lanzamiento de Safari de Apple o en los datos de compatibilidad de MDN, y después compruébala. La propiedad longhand text-wrap-style llegó en Safari 17.5, que incorporó sus valores balance, stable y auto de forma conjunta, así que cualquiera de esos valores sirve limpiamente como barrera para iOS 17.5+:
const isAtLeastIOS175 = () =>
window.CSS?.supports("text-wrap-style", "stable") ?? false;
Verifica tú mismo la correspondencia propiedad↔versión antes de desplegar. Las notas de lanzamiento omiten cambios de vez en cuando, y un indicador de “compatible” puede mentir (consulta la sección anterior). Trata isMobileWebKit() como una heurística sólida, no como una garantía de especificación: Apple permite motores de navegador alternativos en la UE en iOS 17.4+, así que “iOS significa WebKit” es abrumadoramente cierto, pero no absoluto.
Cuándo el sniffing de user-agent es el último recurso acotado
Recurre al user-agent solo cuando se cumplan dos condiciones a la vez. Que la capacidad genuinamente no tenga una prueba de característica, y que una conjetura errónea no te cueste nada peor que un fallo estético. Dos casos de iOS cumplen ese listón.
iPads antiguos. La detección de características no puede distinguir un iPad de un Mac, porque desde iPadOS 13 el user-agent por defecto de un iPad es la misma cadena que envía un Mac. Combina las señales en su lugar. Un user-agent que se lee como Safari de macOS de escritorio, junto con una comprobación positiva de WebKit móvil y un número de puntos táctiles distinto de cero, significa que es un iPad disfrazado de Mac. La prueba de puntos táctiles importa: los Mac informan navigator.maxTouchPoints como 0, así que evita que un Mac real coincida aunque la señal del evento de gestos esté presente en Safari de escritorio.
const looksLikeMacSafari = /Macintosh/.test(navigator.userAgent);
const isIPad = () =>
looksLikeMacSafari && isMobileWebKit() && navigator.maxTouchPoints > 0;
La congelación del UA en iOS 26. En iOS 26 y iPadOS 26, Safari dejó de incluir la versión del sistema operativo en ejecución en su user-agent y fijó el token a una versión anterior. Ese valor fijado ha ido cambiando a lo largo de las versiones puntuales (18_6 en el lanzamiento, después 18_6_2 en Safari 26.1 y luego 18_7 a partir de iOS 26.2), que es exactamente el motivo por el que nunca debes codificarlo de forma fija. Analiza el token Version/, que sigue reflejando la versión mayor real de Safari (y por tanto de iOS):
const iosMajor = () => {
const m = navigator.userAgent.match(/Version\/(\d+)/);
return m ? Number(m[1]) : null; // 26 on iOS 26.x Safari
};
Dos matices afinan esto. Primero, la congelación es exclusiva de Safari: los análisis independientes de registros de servidor de AppleInsider y Lapcat Software confirman que Chrome y Firefox en iOS siguen informando la versión real del sistema operativo, así que “no se puede detectar iOS 26” es un problema específico de Safari, no de iOS en general. Segundo, si prefieres no encargarte tú del parseo, ua-parser-js está en la versión 2.0.10 en su último lanzamiento de npm, así que fija 2.0.10+. El paquete también tiene un historial documentado de incidentes en la cadena de suministro, así que revisa lo que instalas.
El manual
El flujo completo es corto y se ejecuta en orden:
- Detecta características primero.
'x' in window,CSS.supports(),@supports, encadenamiento opcional. Esto cubre la inmensa mayoría de las decisiones condicionales. - Prueba el comportamiento de los mentirosos. Cuando no se puede confiar en un indicador de soporte, renderiza fuera de pantalla y mide con
getBoundingClientRect(). - Infiere la versión de iOS sin UA.
isMobileWebKit()junto con una barrera deCSS.supports()sobre una propiedad confirmada en las notas de lanzamiento. - Haz sniffing solo en los casos límite indetectables. El combinador de iPads antiguos y el token
Version/para la congelación de iOS 26. Úsalos únicamente donde un sniffing fallido no haga perder ninguna funcionalidad. - Prueba en dispositivos reales y simuladores. Tanto las notas de lanzamiento como los indicadores de soporte dejan huecos, y solo el hardware real lo zanja.
La detección de características es la opción por defecto porque sobrevive a actualizaciones que no habías previsto; el sniffing de user-agent es la excepción acotada para los dos o tres casos de iOS que la plataforma hace genuinamente indetectables. Encadena las comprobaciones en ese orden, apóyate en el parseo del token Version/ en lugar de cualquier cadena de sistema operativo congelada, y confirma el resultado en un iPad antiguo real antes de desplegar.
Preguntas frecuentes
¿Se puede detectar iOS 26 en JavaScript a pesar de la congelación del user-agent?
Sí. A partir de iOS 26, Safari congela el token del sistema operativo en su user-agent a un valor anterior a la 26 que ha ido cambiando a lo largo de las versiones puntuales (18_6, después 18_6_2 y luego 18_7 a partir de iOS 26.2), así que codificarlo de forma fija falla. En su lugar, analiza el token Version/, que sigue informando la versión mayor real de Safari y, por tanto, la versión mayor de iOS. En Safari de iOS 26.x, hacer coincidir Version/(\\d+) devuelve 26.
¿La congelación del user-agent en iOS 26 afecta a Chrome y Firefox en iOS?
No. Apple documenta el token congelado del sistema operativo como un comportamiento propio de Safari en iOS y iPadOS 26. Los análisis independientes de registros de servidor de AppleInsider y Lapcat Software confirman que Chrome y Firefox en iOS siguen informando la versión real del sistema operativo en sus cadenas de user-agent, aunque todos los navegadores de iOS se ejecuten sobre WebKit. Así que 'no se puede detectar iOS 26' es un problema específico de Safari, no de iOS en general.
¿Cómo se distingue un iPad de un Mac en el navegador?
Combina dos señales, porque la detección de características por sí sola no puede separarlos: desde iPadOS 13, el user-agent por defecto de un iPad es la misma cadena que envía un Mac. Si el user-agent parece Safari de macOS de escritorio pero una comprobación de WebKit móvil indica que estás en un dispositivo táctil, tienes un iPad haciéndose pasar por un Mac. Como los eventos de gestos también aparecen en Safari de escritorio, condiciona la comprobación a que navigator.maxTouchPoints sea mayor que 0, ya que los Mac informan 0.
¿Funciona navigator.userAgentData en Safari?
No. navigator.userAgentData es exclusivo de Chromium y está marcado como experimental en MDN, así que Safari y Firefox no lo implementan. Nunca es un sustituto viable de la detección de características cuando tu objetivo es Safari en iOS. Comprueba las capacidades directamente con 'x' in window o CSS.supports() en su lugar, un enfoque que MDN considera mucho más fiable que leer el user-agent.