Cambiar el color de la fuente para diferentes fondos con CSS | Programar Plus

Alguna vez has recibido uno de esos, “¡Puedo hacer eso con CSS!” momentos mientras ve a alguien flexionar sus músculos JavaScript? Esa es exactamente la sensación que tuve mientras veía a Dag-Inge Aas e Ida Aalen hablar en CSSconf EU 2018.

Tienen su sede en Noruega, donde la accesibilidad a las WCAG no es solo una buena práctica, sino que en realidad lo exige la ley (¡vaya, Noruega!). Mientras desarrollaban una función que permitía tematizaciones de color seleccionables por el usuario para su producto principal, se enfrentaron a un desafío: ajustar automáticamente el color de la fuente en función del color de fondo seleccionado del contenedor. Si el fondo es oscuro, entonces sería ideal tener un texto blanco para mantenerlo compatible con el contraste WCAG. Pero, ¿qué sucede si en su lugar se selecciona un color de fondo claro? El texto es ilegible y no es accesible.

Utilizaron un enfoque elegante y resolvieron el problema utilizando el paquete npm “color”, agregando bordes condicionales y generación automática de color secundario mientras estaban en ello.

Pero esa es una solución de JavaScript. Aquí está mi alternativa pura a CSS.

El reto

Estos son los criterios que me propuse cumplir:

  • Cambiar la fuente color a blanco o negro dependiendo del color de fondo
  • Aplique el mismo tipo de lógica a los bordes, utilizando una variación más oscura del color base del fondo para mejorar la visibilidad del botón, solo si el fondo es realmente claro
  • Genere automáticamente un color secundario con rotación de tono de 60 °

Trabajar con colores HSL y variables CSS

El enfoque más fácil que podría pensar para esto implica ejecutar colores HSL. Establecer las declaraciones de fondo como HSL donde cada parámetro es una propiedad personalizada de CSS permite una forma realmente simple de determinar la ligereza y usarla como base para nuestras declaraciones condicionales.

:root {
  --hue: 220;
  --sat: 100;
  --light: 81;
}

.btn {
  background: hsl(var(--hue), calc(var(--sat) * 1%), calc(var(--light) * 1%));
}

Esto debería permitirnos cambiar el fondo a cualquier color que queramos en tiempo de ejecución cambiando las variables y ejecutando una declaración if / else para cambiar el color de primer plano.

Excepto … no tenemos declaraciones if / else en CSS … ¿o sí?

Presentación de declaraciones condicionales CSS

Desde la introducción de las variables CSS, también obtuvimos declaraciones condicionales para acompañarlas. O algo así.

Este truco se basa en el hecho de que algunos parámetros CSS se limitan a un valor mínimo y máximo. Por ejemplo, piense en la opacidad. El rango válido es de 0 a 1, por lo que normalmente lo mantenemos allí. Pero también podemos declarar una opacidad de 2, 3 o 1000, y se limitará a 1 y se interpretará como tal. De manera similar, incluso podemos declarar un valor de opacidad negativo y limitarlo a 0.

.something {
  opacity: -2; /* resolves to 0, transparent */
  opacity: -1; /* resolves to 0, transparent */
  opacity: 2; /*resolves to 1, fully opaque */
  opacity: 100; /* resolves to 1, fully opaque */
}

Aplicando el truco a nuestra declaración de color de fuente

El parámetro de luminosidad de una declaración de color HSL se comporta de manera similar, limitando cualquier valor negativo a 0 (que da como resultado negro, cualquiera que sea el tono y la saturación) y cualquier valor superior al 100% está limitado al 100% (que siempre es blanco). ).

Entonces, podemos declarar el color como HSL, restar el umbral deseado del parámetro de luminosidad, luego multiplicar por 100% para forzarlo a sobrepasar uno de los límites (ya sea por debajo de cero o por encima del 100%). Dado que necesitamos que los resultados negativos se resuelvan en blanco y los resultados positivos que se resuelvan en negro, también tenemos que invertirlo multiplicando el resultado por -1.

:root {
  --light: 80;
  /* the threshold at which colors are considered "light." Range: integers from 0 to 100,
recommended 50 - 70 */
  --threshold: 60;
}

.btn {
  /* Any lightness value below the threshold will result in white, any above will result in black */
  --switch: calc((var(--light) - var(--threshold)) * -100%);
  color: hsl(0, 0%, var(--switch));
}

Repasemos ese fragmento de código: a partir de una luminosidad de 80 y considerando un umbral de 60, la resta da como resultado 20, que multiplicado por -100%, da como resultado -2000% limitado a 0%. Nuestro fondo es más claro que el umbral, por lo que lo consideramos claro y aplicamos texto negro.

Si hubiéramos establecido el --light variable como 20, la resta habría resultado en -40, que multiplicado por -100% daría lugar a 4000%, limitado al 100%. Nuestra variable de luz es más baja que el umbral, por lo que lo consideramos un fondo “oscuro” y aplicamos texto blanco para mantener un alto contraste.

Generando un borde condicional

Cuando el fondo de un elemento se vuelve demasiado claro, puede perderse fácilmente contra un fondo blanco. Puede que tengamos un botón y ni siquiera lo notemos. Para proporcionar una mejor interfaz de usuario en colores realmente claros, podemos establecer un borde basado en el mismo color de fondo, solo que más oscuro.

Un fondo claro con un borde oscuro basado en ese color de fondo.

Para lograrlo, podemos usar la misma técnica, pero aplicarla al canal alfa de una declaración HSLA. De esa manera, podemos ajustar el color según sea necesario y luego tenerlo completamente transparente o completamente opaco.

:root {
  /* (...) */
  --light: 85;
  --border-threshold: 80;
}

.btn {
  /* sets the border-color as a 30% darker shade of the same color*/
  --border-light: calc(var(--light) * 0.7%);
  --border-alpha: calc((var(--light) - var(--border-threshold)) * 10);
  
  border: .1em solid hsla(var(--hue), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}

Suponiendo un tono de 0 y una saturación al 100%, el código anterior proporcionará un borde rojo puro completamente opaco al 70% de la luminosidad original si la luminosidad del fondo es superior a 80, o un borde completamente transparente (y por lo tanto, sin borde en todo) si es más oscuro.

Configuración del color secundario con rotación de tono de 60 °

Probablemente el más simple de los desafíos. Hay dos enfoques posibles para ello:

  1. filtro: tono-rotar (60): Esta es la primera que me viene a la mente, pero no es la mejor solución, ya que afectaría el color de los elementos secundarios. Si es necesario, se puede invertir con una rotación opuesta.
  2. Tono HSL + 60: La otra opción es obtener nuestra variable de tono y agregarle 60. Dado que el parámetro de tono no tiene ese comportamiento de limitación en 360, sino que envuelve (como cualquier CSS <angle> type lo hace), debería funcionar sin problemas. Pensar 400deg=40deg, 480deg=120degetc.

Teniendo esto en cuenta, podemos agregar una clase de modificador para nuestros elementos de color secundario que agrega 60 al valor de tono. Dado que las variables que se modifican automáticamente no están disponibles en CSS (es decir, no existe tal cosa como --hue: calc(var(--hue) + 60) ), podemos agregar una nueva variable auxiliar para nuestra manipulación de tono a nuestro estilo base y usarla en la declaración de fondo y borde.

.btn {
  /* (...) */
  --h: var(--hue);
  background: hsl(var(--h), calc(var(--sat) * 1%), calc(var(--light) * 1%));
  border:.1em solid hsla(var(--h), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}

Luego, vuelva a declarar su valor en nuestro modificador:

.btn--secondary {
  --h: calc(var(--hue) + 60);
}

Lo mejor de este enfoque es que adaptará automáticamente todos nuestros cálculos al nuevo tono y los aplicará a las propiedades, debido al alcance de las propiedades personalizadas de CSS.

Y ahí estamos. Poniendolo todo junto, aquí está mi solución CSS pura para los tres desafíos. Esto debería funcionar como un encanto y evitar que incluyamos una biblioteca JavaScript externa.

Consulte el color de fuente de cambio automático de CSS del lápiz según el fondo del elemento…. FAIL de Facundo Corradini (@facundocorradini) en CodePen.

Excepto que no es así. Algunos tonos se vuelven realmente problemáticos (particularmente amarillos y cian), ya que se muestran mucho más brillantes que otros (por ejemplo, rojos y azules) a pesar de tener el mismo valor de luminosidad. En consecuencia, algunos colores se tratan como oscuros y se les da texto blanco a pesar de ser extremadamente brillantes.

En nombre de CSS, ¿qué está pasando?

Introduciendo la ligereza percibida

Estoy seguro de que muchos de ustedes lo habrán notado mucho antes, pero para el resto de nosotros, resulta que la ligereza que percibimos no es la misma que la ligereza de HSL. Afortunadamente, tenemos algunos métodos para sopesar la luminosidad del tono y adaptar nuestro código para que también responda al tono.

Para hacer eso, debemos tener en cuenta la luminosidad percibida de los tres colores primarios dándole a cada uno un coeficiente correspondiente a qué tan claro u oscuro lo percibe el ojo humano. Esto se denomina normalmente luma.

Hay varios métodos para lograrlo. Algunos son mejores que otros en casos específicos, pero ninguno es 100% perfecto. Entonces, seleccioné los dos más populares, que son lo suficientemente buenos:

  • sRGB Luma (Rec. UIT 709): L = (rojo * 0.2126 + verde * 0.7152 + azul * 0.0722) / 255
  • Método W3C (borrador de trabajo): L = (rojo * 0.299 + verde * 0.587 + azul * 0.114) / 255

Implementación de cálculos con corrección luminosa

La primera implicación obvia de optar por un enfoque corregido por luma es que no podemos usar HSL ya que CSS no tiene métodos nativos para acceder a los valores RGB de una declaración HSL.

Por lo tanto, debemos cambiar a una declaración RBG para los fondos, calcular el luma a partir del método que elijamos y usarlo en nuestra declaración de color de primer plano, que puede (y seguirá siendo) HSL.

:root {
  /* theme color variables to use in RGB declarations */
  --red: 200;
  --green: 60;
  --blue: 255;
  /* the threshold at which colors are considered "light". 
  Range: decimals from 0 to 1, recommended 0.5 - 0.6 */
  --threshold: 0.5;
  /* the threshold at which a darker border will be applied.
  Range: decimals from 0 to 1, recommended 0.8+ */
  --border-threshold: 0.8;
}

.btn {
  /* sets the background for the base class */
  background: rgb(var(--red), var(--green), var(--blue));

  /* calculates perceived lightness using the sRGB Luma method 
  Luma = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255 */
  --r: calc(var(--red) * 0.2126);
  --g: calc(var(--green) * 0.7152);
  --b: calc(var(--blue) * 0.0722);
  --sum: calc(var(--r) + var(--g) + var(--b));
  --perceived-lightness: calc(var(--sum) / 255);
  
  /* shows either white or black color depending on perceived darkness */
  color: hsl(0, 0%, calc((var(--perceived-lightness) - var(--threshold)) * -10000000%)); 
}

Para los bordes condicionales, necesitamos convertir la declaración en RGBA y, una vez más, usar el canal alfa para hacerla completamente transparente o completamente opaca. Prácticamente lo mismo que antes, solo ejecutando RGBA en lugar de HSLA. El tono más oscuro se obtiene restando 50 de cada canal.

Hay una De Verdad Error extraño en WebKit en iOS que mostrará un borde negro en lugar del color apropiado si usa variables en los parámetros RGBA del taquigrafía declaración de frontera. La solución es declarar la frontera a mano.
¡Gracias Joel por señalar el error!

.btn {
  /* (...) */
  /* applies a darker border if the lightness is higher than the border threshold */ 
  --border-alpha: calc((var(--perceived-lightness) - var(--border-threshold)) * 100);

  border-width: .2em;
  border-style: solid;
  border-color: rgba(calc(var(--red) - 50), calc(var(--green) - 50), calc(var(--blue) - 50), var(--border-alpha));  
}

Dado que perdimos nuestra declaración de fondo HSL inicial, el color de nuestro tema secundario debe obtenerse mediante la rotación de tonos:

.btn--secondary {
  filter: hue-rotate(60deg);
}

Esto no es lo mejor del mundo. Además de aplicar la rotación de tono a los elementos secundarios potenciales como se discutió anteriormente, significa que el cambio a blanco / negro y la visibilidad del borde en el elemento secundario dependerá del tono del elemento principal y no de sí mismo. Pero por lo que puedo ver, la implementación de JavaScript tiene el mismo problema, así que lo llamaré lo suficientemente cerca.

Y ahí lo tenemos, esta vez para siempre.

Vea el color de fuente de contraste WCAG automático de Pen CSS según el fondo del elemento por Facundo Corradini (@facundocorradini) en CodePen.

Una solución CSS pura que logra el mismo efecto que el enfoque original de JavaScript, pero reduce significativamente el espacio.

Soporte del navegador

IE está excluido debido al uso de variables CSS. Edge no tiene ese comportamiento de limitación que usamos en todo momento. Ve la declaración, la considera una tontería y la descarta por completo como lo haría con cualquier regla rota / desconocida. Todos los demás navegadores principales deberían funcionar.