wandres.dev
RENDERING · Los overlays de depuración

Emular preferencias del sistema, esquema de color y visión

Las emulaciones del panel de renderizado que cambian el contexto en el que se evalúa tu CSS, cómo se combinan, y por qué probar con ellas encuentra bugs que ninguna otra prueba encuentra.

⏱ 16 min

Una parte de tu CSS solo se ejecuta en máquinas configuradas de una forma que probablemente no es la tuya. Nunca la has visto renderizada, nadie del equipo la ha revisado, y sin embargo se sirve a una fracción considerable de tus usuarios. El panel de renderizado incluye una batería de emulaciones que fuerzan esas condiciones sin tocar la configuración del sistema, y recorrerlas una vez por proyecto es una de las auditorías con mejor relación entre coste y hallazgos que existe.

🎯 Al terminar esta lección sabrás
  • Forzar cada preferencia de usuario y comprobar qué cambia en la página.
  • Combinar varias emulaciones para reproducir configuraciones reales.
  • Distinguir la emulación de esquema de color de la de colores forzados.
  • Detectar desde el código el estado de estas preferencias y reaccionar bien.

Las emulaciones disponibles

Todas viven en el panel de renderizado, se activan con desplegables y se acumulan: se pueden tener varias a la vez.

Esquema de color preferido. Fuerza claro u oscuro. Es la más usada y la que más bugs revela, porque el modo oscuro suele implementarse tarde y probarse poco.

Movimiento reducido. Fuerza la preferencia de quien ha pedido al sistema que reduzca las animaciones. Es una preferencia de accesibilidad seria: para personas con trastornos vestibulares, una animación de paralaje puede provocar mareo real.

Contraste preferido. Fuerza la petición de más o menos contraste.

Transparencia reducida. Fuerza la preferencia de quien no quiere superficies translúcidas.

Colores forzados. Emula el modo de alto contraste del sistema operativo, en el que el sistema sustituye tus colores por su propia paleta. Es distinto del modo oscuro y mucho menos conocido, y es donde aparecen los bugs más graves.

Tipo de medio. Cambia entre pantalla e impresión, lo que permite revisar los estilos de impresión sin abrir el diálogo correspondiente.

Gama de color. Emula pantallas con distinta capacidad de reproducción de color, útil si usas colores fuera del espacio tradicional.

Deficiencias de visión. Simula varios tipos de daltonismo, visión borrosa y ausencia de percepción del color. Se trató a fondo desde el ángulo de accesibilidad en simular deficiencias de visión; aquí el ángulo es distinto y complementario: combinarlas con las demás emulaciones, porque un problema de contraste en modo oscuro con daltonismo es un caso que ninguna de las dos pruebas por separado detecta.

ℹ️
Nota

Las emulaciones afectan a la página, no a las DevTools ni al navegador, y se desactivan al cerrar el panel. Eso significa que no puedes olvidarte una puesta y confundirte durante días, pero también que hay que reactivarlas en cada sesión. Si vas a hacer una auditoría completa, el orden que funciona es recorrer las preferencias una por una con la página quieta, y después combinarlas.

Qué encuentra cada una

El esquema de color encuentra sobre todo tres cosas: colores escritos a mano que no se adaptaron, imágenes con fondo blanco que quedan como un recuadro luminoso en una pantalla oscura, y sombras que en fondo oscuro desaparecen o quedan ridículas. Si el proyecto usa variables de color bien organizadas, la mayoría se resuelve sola; si tiene colores literales repartidos, aparecerán todos de golpe.

El movimiento reducido encuentra animaciones que no se desactivan. La implementación correcta no es eliminar todo movimiento sino reducirlo a algo que no provoque sensación de desplazamiento: una transición de opacidad en lugar de un deslizamiento, una duración muy corta en lugar de ninguna.

/* Respetar la preferencia sin eliminar toda la retroalimentacion visual */
.panel {
  transition: transform 320ms ease, opacity 320ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: opacity 120ms linear;
    transform: none !important;
  }
  /* Detener animaciones decorativas en curso */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

Los colores forzados encuentran la categoría más grave: interfaces que dejan de ser usables. En ese modo el sistema impone su paleta, y todo lo que comunicara información solo mediante color desaparece. Un botón cuyo estado activo se distingue por el fondo, un campo con error señalado únicamente por un borde rojo, un icono dibujado como imagen de fondo: todo eso se vuelve indistinguible. Los arreglos son concretos: usar los colores del sistema en lugar de los propios cuando el modo está activo, y no depender nunca del color como único canal de información.

/* Adaptarse cuando el sistema impone su paleta */
@media (forced-colors: active) {
  .boton-primario {
    /* Los colores del sistema si se respetan en este modo */
    background-color: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonText;
  }
  .boton-primario[aria-pressed="true"] {
    /* Un borde mas grueso comunica el estado sin depender del color */
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }
  /* Los iconos como imagen de fondo desaparecen: hay que forzarlos */
  .icono-decorativo { forced-color-adjust: none; }
}

Detectar y reaccionar desde el código

Las mismas preferencias son consultables desde JavaScript, y esa consulta es necesaria cuando el comportamiento no se puede expresar solo en CSS: animaciones programáticas, gráficos en lienzo, decisiones sobre qué cargar.

// Lee y vigila todas las preferencias del usuario relevantes
(() => {
  const consultas = {
    esquemaOscuro: '(prefers-color-scheme: dark)',
    movimientoReducido: '(prefers-reduced-motion: reduce)',
    masContraste: '(prefers-contrast: more)',
    menosContraste: '(prefers-contrast: less)',
    transparenciaReducida: '(prefers-reduced-transparency: reduce)',
    coloresForzados: '(forced-colors: active)',
    gamaAmplia: '(color-gamut: p3)',
    punteroGrueso: '(pointer: coarse)',
    puedeHover: '(hover: hover)'
  };

  const estado = {};
  for (const [nombre, consulta] of Object.entries(consultas)) {
    const mq = matchMedia(consulta);
    estado[nombre] = mq.matches;
    mq.addEventListener('change', e => {
      estado[nombre] = e.matches;
      console.log('Cambio de preferencia:', nombre, '->', e.matches);
      document.documentElement.dataset[nombre] = String(e.matches);
    });
    document.documentElement.dataset[nombre] = String(mq.matches);
  }

  console.table([estado]);
  window.preferencias = estado;

  // Uso practico: una animacion que se adapta sola
  window.animarRespetando = (el, distancia = 200) => {
    const reducido = estado.movimientoReducido;
    return el.animate(
      reducido
        ? [{ opacity: 0 }, { opacity: 1 }]
        : [{ opacity: 0, transform: `translateY(${distancia}px)` }, { opacity: 1, transform: 'none' }],
      { duration: reducido ? 120 : 420, easing: 'ease-out', fill: 'both' }
    );
  };
})();

El detalle de escribir cada preferencia como atributo de datos en el elemento raíz es un patrón práctico: permite escribir CSS que reaccione a la preferencia con selectores de atributo, útil cuando hace falta combinar la preferencia con estado de la aplicación de formas que las consultas de medios no expresan bien.

Las dos últimas consultas de la lista no son preferencias sino capacidades del dispositivo, y aparecen aquí porque se emulan en el mismo sitio, en el modo de dispositivo: si tu interfaz depende de que exista hover, en un dispositivo táctil esa parte no existe, y esa es la causa de menús que no se pueden abrir en móvil.

Estas preferencias no son gustos estéticos: son adaptaciones que alguien configuró por necesidad

Merece la pena reencuadrar mentalmente qué son estos ajustes, porque el encuadre habitual lleva a tratarlos como una cortesía opcional y a priorizarlos en consecuencia. Nadie activa el modo de colores forzados porque le guste cómo queda. Lo activa quien necesita ese nivel de contraste para leer, y a partir de ese momento navega por toda la web así, incluida tu aplicación. Nadie activa el movimiento reducido por preferencia estética: lo activa quien se marea con las transiciones, y una animación de paralaje que a ti te parece elegante le produce a esa persona un malestar físico real que puede durar horas. La preferencia de más contraste, la de menos transparencia, el tamaño de fuente aumentado del sistema: todas son adaptaciones que alguien configuró una vez, deliberadamente, porque sin ellas no puede usar el ordenador cómodamente. Eso cambia la lectura de lo que significa ignorarlas. Una aplicación que no respeta el movimiento reducido no está siendo poco pulida: está sobrescribiendo activamente una decisión médica del usuario. Una que se vuelve inutilizable con colores forzados no tiene un defecto cosmético: excluye a un grupo concreto de personas de usarla. Y hay un argumento adicional, más frío, que conviene tener a mano en las discusiones de prioridades: estos usuarios no son un grupo marginal y además tienden a ser los más leales, porque una vez encuentran un producto que funciona con su configuración, se quedan. El coste de dar soporte es además llamativamente bajo comparado con casi cualquier otra tarea: recorrer las emulaciones del panel de renderizado en una aplicación mediana cuesta media hora, y los arreglos que produce suelen ser de unas pocas reglas de CSS y de sustituir algún indicador que dependía solo del color. Media hora una vez por proyecto, y el resultado es que la aplicación deja de expulsar a personas que ni siquiera te van a escribir para contártelo: simplemente se van y usan otra cosa.