Más allá del texto: contraste, movimiento y ajustes del sistema
La accesibilidad visual no termina en el lector de pantalla. El contraste insuficiente, el color como único portador de significado, las animaciones que provocan náuseas y los materiales translúcidos que devoran la legibilidad afectan a millones de personas que nunca activarán VoiceOver. Esta lección recorre los umbrales medibles, los ajustes del sistema que llegan al entorno de SwiftUI y la disciplina de responder a ellos en lugar de ignorarlos.
Hay un sesgo cómodo en la forma en que los equipos abordan la accesibilidad: se identifica con la ceguera, se resuelve con etiquetas y se da por cerrada. Pero la inmensa mayoría de las personas afectadas por decisiones de interfaz no usa lector de pantalla. Usa la vista, con menos contraste del que tú tienes, con una percepción del color distinta de la tuya, o con un sistema vestibular que reacciona con náusea a una transición que a ti te parece elegante. Estas personas no aparecen en ninguna auditoría de etiquetas porque no fallan al navegar: fallan al percibir, y el fallo es silencioso. Nadie escribe una reseña diciendo que no distingue el estado activo de un interruptor; simplemente deja de usar la app.
- Aplicar los umbrales de contraste exigibles y calcularlos sobre pares de color reales.
- Eliminar la dependencia del color como único canal de información y añadir redundancia.
- Responder correctamente a los ajustes de movimiento, transparencia, contraste y negrita.
- Verificar la respuesta a esos ajustes en previsualizaciones y en dispositivo.
Contraste: un umbral, no una opinión
El contraste entre dos colores se define como el cociente entre sus luminancias relativas más un desplazamiento, y produce un número entre uno y veintiuno. Los criterios de las WCAG fijan los mínimos con precisión: 4.5 a 1 para el texto normal, 3 a 1 para el texto grande —a partir de dieciocho puntos, o catorce en negrita— y 3 a 1 para los componentes de interfaz y los objetos gráficos que transmiten información, como el borde de un campo, el trazo de una gráfica o el estado de un interruptor. El nivel triple A eleva el texto normal a 7 a 1, y las directrices de diseño de Apple recomiendan tomarlo como aspiración cuando el diseño lo permita.
La consecuencia práctica es que el contraste deja de ser materia de gusto. Un gris claro sobre blanco no es discutiblemente elegante: es medible, y si da 2.9 no cumple. Ese carácter aritmético es también una buena noticia, porque permite automatizar la comprobación en la fase de diseño y en integración continua sobre la paleta, en lugar de descubrirla en revisión.
El primero es medir sobre el color nominal en vez de sobre el color compuesto: un texto con opacidad reducida sobre un fondo con material translúcido no tiene el contraste que dice la paleta, sino el que resulta tras la mezcla. El segundo es medir solo en un esquema: una pareja que cumple en claro puede fallar en oscuro, donde el mismo acento pierde separación respecto al fondo. Mide siempre el color final, en los dos esquemas y con el ajuste de contraste aumentado activo.
La plataforma ayuda si se la deja. Los colores semánticos del sistema —etiqueta primaria, secundaria, terciaria, fondos agrupados, separadores— ya están calibrados para cumplir en claro, en oscuro y con el contraste aumentado. Y para la paleta propia, el catálogo de recursos admite variantes por apariencia y por contraste alto, de modo que un mismo nombre resuelve al color adecuado sin condicionales en el código.
Text("Pendiente")
.foregroundStyle(.secondary) // semantico: calibrado por el sistema
.background(Color("Aviso")) // del catalogo, con variante de alto contraste
El color no puede ir solo
El daltonismo afecta a cerca del ocho por ciento de los hombres de ascendencia europea, y sus formas más comunes comprometen justo el eje rojo verde que la industria del software usa sin descanso para codificar error y éxito. Una lista donde el estado se distingue por un punto rojo o verde no comunica nada a esas personas, y tampoco a quien mira la pantalla bajo el sol o con un filtro nocturno agresivo. El principio es antiguo y sigue vigente: el color puede reforzar la información, nunca portarla en exclusiva.
Forma
Un símbolo distinto por estado: triángulo para aviso, círculo para información, cruz para error. La silueta sobrevive a cualquier alteración cromática.
Texto
Una palabra corta junto al indicador. Es el canal más robusto y el único que funciona igual para la vista, la voz y el braille.
Patrón o posición
Trama, grosor de trazo o un lugar fijo en la fila. En gráficas, etiquetar la serie directamente sobre la línea supera a cualquier leyenda de colores.
SwiftUI expone el ajuste del sistema mediante accessibilityDifferentiateWithoutColor, pensado exactamente para reforzar la codificación cuando la persona lo ha pedido. La forma madura de usarlo es no depender de él: diseñar con redundancia desde el principio y reservar el ajuste para intensificar, no para introducir la única alternativa.
@Environment(\.accessibilityDifferentiateWithoutColor) private var sinColor
HStack {
Image(systemName: estado.simbolo) // forma distinta por estado
.foregroundStyle(estado.tinte) // el color solo refuerza
Text(estado.titulo) // y el texto lo dice
if sinColor { Text(estado.abreviatura).bold() }
}
Movimiento, transparencia y compañía
El movimiento a gran escala —desplazamientos amplios, zoom, paralaje, rebotes— desencadena en algunas personas mareo, náusea y desorientación por conflicto vestibular. No es una molestia estética: es una respuesta fisiológica que puede durar horas. El ajuste de reducir movimiento existe para eso, y responder a él no significa eliminar toda animación sino sustituir la traslación por una atenuación. Una transición que aparece con un fundido cruzado sigue comunicando el cambio de estado sin desplazar el campo visual.
@Environment(\.accessibilityReduceMotion) private var menosMovimiento
var body: some View {
Panel()
.transition(menosMovimiento ? .opacity : .slide)
.animation(menosMovimiento ? .none : .spring(duration: 0.4), value: visible)
}
Conviene precisar qué se considera movimiento problemático, porque la reacción excesiva también es un fallo. Lo que desencadena el conflicto vestibular es el desplazamiento de grandes superficies, el escalado brusco, el paralaje y las trayectorias curvas rápidas. Un cambio de opacidad, un color que transiciona o un indicador que gira en un espacio pequeño no entran en esa categoría y no hace falta suprimirlos. Eliminar toda animación cuando el ajuste está activo suele empeorar la comprensión, porque desaparecen las señales de continuidad que explican de dónde viene cada cosa.
Los demás ajustes siguen el mismo patrón: el sistema los publica en el entorno y tú decides qué significan en tu interfaz. Reducir transparencia debe sustituir los materiales translúcidos por fondos sólidos, porque un material sobre contenido variable produce contrastes impredecibles. El contraste aumentado, disponible como colorSchemeContrast, debe intensificar bordes y separadores. El texto en negrita del sistema llega como legibilityWeight y afecta también a los símbolos. Y mostrar formas de botón exige que lo pulsable se vea pulsable sin depender del color del rótulo.
flowchart TB a[Ajustes de accesibilidad del sistema] --> b[Valores en el Environment de SwiftUI] b --> c[reduceMotion: fundido en vez de desplazamiento] b --> d[reduceTransparency: fondo solido] b --> e[colorSchemeContrast: bordes marcados] b --> f[legibilityWeight: tipografia mas gruesa] b --> g[differentiateWithoutColor: forma y texto] c --> h[La interfaz responde sin preguntar] d --> h e --> h f --> h g --> h
Queda una dimensión que rara vez se cuenta junto a las anteriores y pertenece al mismo capítulo: el tamaño de los objetivos táctiles. Las directrices de Apple fijan cuarenta y cuatro puntos como mínimo para cualquier elemento accionable, y ese número no protege solo a quien tiene temblor o movilidad reducida: protege a cualquiera que use la app caminando, con guantes o con prisa. Un icono de veinticuatro puntos puede seguir midiendo veinticuatro visualmente si se le añade área táctil con relleno o con un marco mayor y un fondo transparente que responda al tacto.
Button(action: cerrar) {
Image(systemName: "xmark")
.frame(width: 44, height: 44) // area tactil, no tamano visual
.contentShape(Rectangle()) // toda el area responde, no solo el glifo
}
contentShape es la pieza que casi siempre falta: sin ella, la zona sensible se limita al trazo dibujado del símbolo, de modo que el marco ampliado engaña al desarrollador —que ve el espacio— sin ayudar a la persona, que sigue teniendo que acertar en un glifo de doce puntos. Conviene revisar además que dos objetivos ampliados no acaben solapándose, porque un toque ambiguo es peor que un toque difícil.
Probar que la app responde
Todo lo anterior es inútil si nadie comprueba que ocurre. La ventaja de estos ajustes frente a otras dimensiones de la accesibilidad es que su verificación se automatiza con facilidad, porque son valores del entorno y el entorno se puede fijar en una previsualización o en un test. La práctica que mejor rinde consiste en no crear previsualizaciones nuevas por cada combinación, sino declarar un conjunto pequeño de escenarios representativos y aplicarlos a las vistas críticas.
#Preview("Contraste y movimiento reducidos") {
Panel()
.environment(\.accessibilityReduceMotion, true)
.environment(\.accessibilityReduceTransparency, true)
.environment(\.colorSchemeContrast, .increased)
.preferredColorScheme(.dark)
}
Hay un matiz técnico que conviene conocer para no perder tiempo: fijar estos valores en el entorno de una previsualización afecta a cómo tu código reacciona, pero no reproduce exactamente lo que hace el sistema operativo con sus propios materiales y efectos. Para eso hacen falta los interruptores reales, y ahí el Accessibility Inspector permite activarlos sobre la app en ejecución en un dispositivo físico, que es donde aparecen las sorpresas con materiales, sombras y símbolos.
La comprobación de contraste, por su parte, no debería vivir en la app sino en la paleta. Si los colores están definidos en un catálogo de recursos o en un paquete de sistema de diseño, el cálculo del cociente de luminancias entre las combinaciones permitidas se puede ejecutar como prueba unitaria sobre esa fuente única y fallar en integración continua cuando alguien introduzca un par que no cumple. Es una prueba de veinte líneas que impide una clase entera de defectos antes de que llegue a una pantalla.
Cada vez que una información viaje por un solo canal, pregúntate cuál es el segundo. Un estado que solo se ve puede anunciarse; una alerta que solo suena puede vibrar con sensoryFeedback y mostrar texto; un progreso que solo se dibuja puede exponer valor accesible. La redundancia sensorial es barata de implementar, imposible de retrofitar limpiamente y beneficia a todo el mundo en contextos ruidosos, luminosos o de prisa.
Conviene ver lo que ocurre realmente cuando alguien activa reducir movimiento en los ajustes. Esa persona no está configurando una app: está declarando una condición sobre todo el dispositivo, con la expectativa razonable de que el sistema operativo la haga cumplir en todas partes. iOS honra esa declaración en sus propias transiciones, en el conmutador de apps, en el desbloqueo. Y entonces abre tu app, que decide que su animación es demasiado bonita para renunciar a ella, y la promesa se rompe justo en el punto donde la persona no puede hacer nada al respecto. El daño no es solo el mareo: es que un ajuste del sistema que a veces se cumple y a veces no deja de ser un ajuste y pasa a ser una lotería, y con ello se destruye la única propiedad que hacía útil configurarlo. Esta es la razón por la que responder a los ajustes de accesibilidad no es cortesía sino integridad de plataforma: cada app que los ignora degrada la confianza en el sistema entero, del mismo modo que una biblioteca que ignora la cancelación de tareas degrada la confianza en el modelo de concurrencia. Y hay una implicación de diseño que se sigue de ahí y que separa a los equipos que entienden de los que cumplen: los ajustes no son excepciones que haya que parchear al final, son entradas de tu función de interfaz exactamente igual que el tamaño de texto, el esquema de color o el tamaño de la ventana. Una vista que se escribe leyendo el entorno desde el primer día no necesita después una capa de condicionales defensivos; se limita a ser, desde su definición, una función de todo su contexto.
Los umbrales son 4.5 a 1 para texto normal, 3 a 1 para texto grande y para componentes gráficos, medidos sobre el color compuesto final y en ambos esquemas. El color nunca debe ser el único portador de significado: añade forma, texto o posición. Responde a reducir movimiento con fundidos en lugar de desplazamientos, a reducir transparencia con fondos sólidos, al contraste aumentado con bordes marcados y a la negrita del sistema con tipografía más gruesa. Y mantén cuarenta y cuatro puntos de área táctil aunque el dibujo mida menos.
- Calcula el contraste de los diez pares de color más frecuentes de tu app en claro y en oscuro, y marca en rojo los que no lleguen a 4.5 a 1 sobre el color compuesto real.
- Busca todo lugar donde el estado se codifique solo con color y añade un segundo canal; verifica el resultado con un filtro de simulación de deuteranopia.
- Activa reducir movimiento y recorre la app entera anotando cada transición que siga desplazando contenido; conviértelas en fundidos sin perder la señal de cambio.
- Activa reducir transparencia y contraste aumentado a la vez y localiza los materiales y separadores que dejan de funcionar.
- Mide con una retícula los objetivos táctiles menores de cuarenta y cuatro puntos y amplía su área sin cambiar el dibujo, comprobando que no se solapan entre sí.