Programación de Sass para crear combinaciones de colores accesibles | Programar Plus

Todos buscamos frutas que estén al alcance de la mano para hacer que nuestros sitios y aplicaciones sean más accesibles. Una de las cosas más fáciles que podemos hacer es asegurarnos de que los colores que usamos sean agradables a la vista. El alto contraste de color es algo que beneficia a todos. No solo reduce la fatiga visual en general, sino que es crucial para las personas que padecen de visión reducida.

Entonces, no solo usemos mejores combinaciones de colores en nuestros diseños, sino que busquemos una manera de facilitarnos la implementación de altos contrastes. Hay una estrategia específica que usamos en Oomph que permite que una función Sass haga todo el trabajo pesado por nosotros. Te guiaré a través de cómo armamos eso.

¿Quiere ir directamente al código porque ya comprende todo lo que hay que saber sobre la accesibilidad del color? Aqui tienes.

Qué entendemos por “combinaciones de colores accesibles”

El contraste de color también es una de esas cosas que podemos pensar que hemos manejado. Pero los altos contrastes de color implican más que solo mirar un diseño. Hay diferentes niveles de criterios aceptables que la WCAG ha definido como accesibles. En realidad, es una lección de humildad abrir el Comprobador de contraste de WebAIM y ejecutar las combinaciones de colores de un sitio a través de él.

Mi equipo se adhiere a las pautas de nivel AA de WCAG de forma predeterminada. Esto significa que:

  • El texto de 24 píxeles y más grande, o de 19 píxeles y más grande si está en negrita, debe tener una relación de contraste de color (CCR) de 3.0: 1.
  • El texto de menos de 24 píxeles debe tener un CCR de 4.5: 1.

Si un sitio necesita adherirse a las pautas mejoradas para el nivel AAA, los requisitos son un poco más altos:

  • El texto de 24 píxeles y más grande, o de 19 píxeles y más grande si está en negrita, debe tener un CCR de 4.5: 1.
  • El texto de menos de 24 píxeles debe tener un CCR de 7: 1.

Ratios? ¿Eh? Sí, hay algunas matemáticas involucradas aquí. Pero la buena noticia es que no es necesario que lo hagamos nosotros mismos o incluso que tengamos el mismo conocimiento profundo sobre cómo se calculan de la forma en que Stacie Arellano compartió recientemente (que es una lectura obligada si te gusta la ciencia de la accesibilidad del color). ).

Ahí es donde entra en juego Sass. Podemos aprovecharlo para ejecutar cálculos matemáticos difíciles que de otro modo volarían sobre muchas de nuestras cabezas. Pero primero, creo que vale la pena tratar con colores accesibles a nivel de diseño.

Las paletas de colores accesibles comienzan con los diseños.

Eso es correcto. El núcleo del trabajo de crear una paleta de colores accesible comienza con los diseños. Idealmente, cualquier diseño web debería consultar una herramienta para verificar que cualquier combinación de colores en uso pase las pautas establecidas, y luego modificar los colores que no lo hacen. Cuando nuestro equipo de diseño hace esto, utiliza una herramienta que desarrollamos internamente. Funciona en una lista de colores, probándolos sobre un color oscuro y uno claro, además de proporcionar una forma de probar otras combinaciones.

ColorCube proporciona una descripción general de una paleta de colores completa, que muestra cómo se desempeña cada color cuando se combina con blanco, negro e incluso entre sí. Incluso muestra resultados para los niveles AA y AAA de WCAG junto a cada resultado. La herramienta fue diseñada para arrojar mucha información al usuario de una sola vez al evaluar una lista de colores.

Esto es lo primero que hace nuestro equipo. Me atrevería a suponer que muchos colores de marca no se eligen con la accesibilidad a la vanguardia. A menudo encuentro que esos colores deben cambiar cuando se traducen a un diseño web. A través de la educación, la conversación y las muestras visuales, conseguimos que el cliente apruebe la nueva paleta de colores. Lo admito: esa parte puede ser más difícil que el trabajo real de implementar combinaciones de colores accesibles.

La auditoría de contraste de color: una entrega de diseño típica cuando se trabaja con la paleta de colores de una marca existente. Aquí, sugerimos dejar de usar el color de la marca Emerald con blanco, pero use una versión “Alt” que sea un poco más oscura.

El problema que quería resolver con la automatización es los casos de borde. No se puede culpar a un diseñador por perderse alguna instancia en la que dos colores se combinan de manera no deseada, simplemente sucede. Y esos casos extremos aparecerán, ya sea durante la construcción o incluso un año después, cuando se agreguen nuevos colores al sistema.

Desarrollar para la accesibilidad mientras se mantiene fiel a la intención de un sistema de color

El truco para cambiar los colores para cumplir con los requisitos de accesibilidad es no cambiarlos tanto que ya no se vean como el mismo color. Una marca que ama su color verde esmeralda querrá mantener la intención de ese color: es “esmeralda”. Para que pase por accesibilidad cuando se usa como texto sobre un fondo blanco, es posible que tengamos que oscurecer el verde y aumentar su saturación. Pero aún queremos que el color se “lea” igual que el color original.

Para lograr esto, usamos el modelo de color Hue Saturation Lightness (HSL). HSL nos da la capacidad de mantener el tono como está pero ajustar la saturación (es decir, aumentar o disminuir el color) y la luminosidad (es decir, agregar más negro o más blanco). El tono es lo que hace que un verde sea tan verde o un azul tan azul. Es el “alma” del color, para volverse un poco místico al respecto.

El tono se representa como una rueda de color con un valor entre 0 ° y 360 °: amarillo a 60 °, verde a 120 °, cian a 180 °, etc. La saturación es un porcentaje que va del 0% (sin saturación) al 100% ( saturación completa). La luminosidad también es un valor que va del 0% al 100%, donde no hay luminosidad al 0%, no hay negro ni blanco al 50%, y 100% es todo luminosidad, o muy claro.

Una vista rápida de cómo se ve el ajuste de un color en nuestra herramienta:

Con HSL, cambiar el verde de bajo contraste a un mayor contraste significó cambiar la saturación de 63 a 95 y la luminosidad de 45 a 26 a la izquierda. Ahí es cuando el color obtiene una marca de verificación verde en el medio cuando se usa con blanco. Sin embargo, el nuevo verde todavía se siente como si estuviera en la misma familia, porque el tono se mantuvo en 136, que es como el “alma” del color.

Para obtener más información, juegue con el divertido visualizador HSL mothereffinghsl.com. Pero para una descripción más detallada del daltonismo, los niveles de contraste de color WCAG y el espacio de color HSL, escribimos una publicación de blog en profundidad al respecto.

El caso de uso que quiero resolver

Los diseñadores pueden ajustar los colores con las herramientas que acabamos de revisar, pero hasta ahora, ningún Sass que he encontrado podría hacerlo con magia matemática. Tenía que haber una forma.

Estos son algunos enfoques similares que he visto en la naturaleza:

  • Una idea de Josh Bader utiliza variables CSS y colores divididos en sus valores RGB para calcular si el blanco o el negro es el mejor color accesible para usar en una situación determinada.
  • Otra idea de Facundo Corradini hace algo similar con los valores HSL y una “función de cambio” muy interesante en CSS.

No me gustaron estos enfoques. No quería volver al blanco o al negro. Quería que se mantuvieran los colores pero que se ajustaran para que fueran accesibles. Además, cambiar los colores de sus componentes RGB o HSL y almacenarlos con variables CSS parecía complicado e insostenible para una base de código grande.

Quería usar un preprocesador como Sass para hacer esto: dados dos colores, ajuste automágicamente uno de ellos para que el par reciba una calificación aprobatoria de WCAG. Las reglas establecen algunas otras cosas a considerar también: el tamaño del texto y si la fuente está en negrita o no. La solución tenía que tener esto en cuenta.

En términos de código, quería hacer esto:

// Transform this non-passing color pair:
.example {
  background-color: #444;
  color: #0094c2; // a 2.79 contrast ratio when AA requires 4.5
  font-size: 1.25rem;
  font-weight: normal;
}


// To this passing color pair:
.example {
  background-color: #444;
  color: #00c0fc; // a 4.61 contrast ratio
  font-size: 1.25rem;
  font-weight: normal;
}

Una solución que haga esto podría detectar y manejar esos casos extremos que mencionamos anteriormente. Quizás el diseñador tuvo en cuenta un azul de marca para usar sobre un azul claro, pero no un gris claro. Tal vez el rojo que se usa en los mensajes de error deba modificarse para este formulario que tiene un color de fondo único. Tal vez queramos implementar una función de modo oscuro en la interfaz de usuario sin tener que volver a probar todos los colores. Estos son los casos de uso que tenía en mente al abordar esto.

Con las fórmulas puede venir la automatización

El W3C ha proporcionado a la comunidad fórmulas que ayudan a analizar dos colores usados ​​juntos. La fórmula multiplica los canales RGB de ambos colores por números mágicos (un peso visual basado en cómo los humanos perciben estos canales de color) y luego los divide para obtener una proporción de 0.0 (sin contraste) a 21.0 (todo el contraste, solo posible con blanco y negro). Si bien es imperfecta, esta es la fórmula que usamos en este momento:

If L1 is the relative luminance of a first color 
And L2 is the relative luminance of a second color, then
- Color Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Where
- L = 0.2126 * R + 0.7152 * G + 0.0722 * B
And
- if R sRGB <= 0.03928 then R = R sRGB /12.92 else R = ((R sRGB +0.055)/1.055) ^ 2.4
- if G sRGB <= 0.03928 then G = G sRGB /12.92 else G = ((G sRGB +0.055)/1.055) ^ 2.4
- if B sRGB <= 0.03928 then B = B sRGB /12.92 else B = ((B sRGB +0.055)/1.055) ^ 2.4
And
- R sRGB = R 8bit /255
- G sRGB = G 8bit /255
- B sRGB = B 8bit /255

Si bien la fórmula parece compleja, es solo matemática, ¿verdad? Bueno, no tan rápido. Hay una parte al final de algunas líneas donde el valor se multiplica por una potencia decimal, elevado a la potencia de 2,4. ¿Darse cuenta de? Resulta que son matemáticas complejas que la mayoría de los lenguajes de programación pueden lograr: piense en Javascript math.pow() función, pero Sass no es lo suficientemente poderoso para hacerlo.

Tiene que haber otra forma …

Por supuesto que la hay. Solo tomó algo de tiempo encontrarlo. 🙂

Mi primera versión usó una serie compleja de cálculos matemáticos que hicieron el trabajo de poderes decimales dentro de los límites limitados de lo que Sass puede lograr. Al buscar en Google, la gente es mucho más inteligente que yo al proporcionar las funciones. Desafortunadamente, calcular solo un puñado de combinaciones de contraste de color aumentó exponencialmente los tiempos de construcción de Sass. Entonces, eso significa que Sass puede hacerlo, pero eso no significa que deba hacerlo. En producción, los tiempos de compilación para una base de código grande podrían aumentar a varios minutos. Eso no es aceptable.

Después de buscar más en Google, encontré una publicación de alguien que estaba tratando de hacer algo similar. También se encontraron con la falta de soporte de exponentes en Sass. Querían explorar “la posibilidad de utilizar la aproximación newtoniana para las partes fraccionarias del exponente”. Entiendo totalmente el impulso (no). En su lugar, decidieron utilizar una “tabla de búsqueda”. Es una solución genial. En lugar de hacer los cálculos desde cero cada vez, una tabla de búsqueda proporciona todas las respuestas posibles precalculadas. La función Sass recupera la respuesta de la lista y está listo.

En sus palabras:

La unica parte [of the Sass that] implica exponenciación son las conversiones de espacio de color por canal realizadas como parte del cálculo de luminancia. [T]aquí hay solo 256 valores posibles para cada canal. Esto significa que podemos crear fácilmente una tabla de búsqueda.

Ahora estamos cocinando. Había encontrado una dirección más eficaz.

Ejemplo de uso

El uso de la función debe ser fácil y flexible. Dado un conjunto de dos colores, ajuste el primer color para que pase el valor de contraste correcto para el nivel de WCAG dado cuando se use con el segundo color. Los parámetros opcionales también tendrán en cuenta el tamaño de fuente o la negrita.

// @function a11y-color(
//   $color-to-adjust,
//   $color-that-will-stay-the-same,
//   $wcag-level: 'AA',
//   $font-size: 16,
//   $bold: false
// );


// Sass sample usage declaring only what is required
.example {
  background-color: #444;
  color: a11y-color(#0094c2, #444); // a 2.79 contrast ratio when AA requires 4.5 for small text that is not bold
}


// Compiled CSS results:
.example {
  background-color: #444;
  color: #00c0fc; // which is a 4.61 contrast ratio
}

Usé una función en lugar de un mixin porque prefería la salida de un valor único independiente de una regla CSS. Con una función, el autor puede determinar qué color debe cambiar.

Un ejemplo con más parámetros implementados se ve así:

// Sass
.example-2 {
  background-color: a11y-color(#0094c2, #f0f0f0, 'AAA', 1.25rem, true); // a 3.06 contrast ratio when AAA requires 4.5 for text 19px or larger that is also bold
  color: #f0f0f0;
  font-size: 1.25rem;
  font-weight: bold;
}


// Compiled CSS results:
.example-2 {
  background-color: #087597; // a 4.6 contrast ratio
  color: #f0f0f0;
  font-size: 1.25rem;
  font-weight: bold;
}

Una inmersión más profunda en el corazón de la función Sass

Para explicar el enfoque, veamos qué está haciendo la función final, línea por línea. Hay muchas funciones auxiliares en el camino, pero los comentarios y la lógica en la función principal explican el enfoque:

// Expected:
// $fg as a color that will change
// $bg as a color that will be static and not change
// Optional:
// $level, default 'AA'. 'AAA' also accepted
// $size, default 16. PX expected, EM and REM allowed
// $bold, boolean, default false. Whether or not the font is currently bold
//
@function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false) {
  // Helper: make sure the font size value is acceptable
  $font-size: validate-font-size($size);
  // Helper: With the level, font size, and bold boolean, return the proper target ratio. 3.0, 4.5, or 7.0 results expected
  $ratio: get-ratio($level, $font-size, $bold);
  // Calculate the first contrast ratio of the given pair
  $original-contrast: color-contrast($fg, $bg);
  
  @if $original-contrast >= $ratio {
    // If we pass the ratio already, return the original color
    @return $fg;
  } @else {
    // Doesn't pass. Time to get to work
    // Should the color be lightened or darkened?
    // Helper: Single color input, 'light' or 'dark' as output
    $fg-lod: light-or-dark($fg);
    $bg-lod: light-or-dark($bg);


    // Set a "step" value to lighten or darken a color
    // Note: Higher percentage steps means faster compile time, but we might overstep the required threshold too far with something higher than 5%
    $step: 2%;
    
    // Run through some cases where we want to darken, or use a negative step value
    @if $fg-lod == 'light' and $bg-lod == 'light' {
      // Both are light colors, darken the fg (make the step value negative)
      $step: - $step;
    } @else if $fg-lod == 'dark' and $bg-lod == 'light' {
      // bg is light, fg is dark but does not pass, darken more
      $step: - $step;
    }
    // Keeping the rest of the logic here, but our default values do not change, so this logic is not needed
    //@else if $fg-lod == 'light' and $bg-lod == 'dark' {
    //  // bg is dark, fg is light but does not pass, lighten further
    //  $step: $step;
    //} @else if $fg-lod == 'dark' and $bg-lod == 'dark' {
    //  // Both are dark, so lighten the fg
    //  $step: $step;
    //}
    
    // The magic happens here
    // Loop through with a @while statement until the color combination passes our required ratio. Scale the color by our step value until the expression is false
    // This might loop 100 times or more depending on the colors
    @while color-contrast($fg, $bg) < $ratio {
      // Moving the lightness is most effective, but also moving the saturation by a little bit is nice and helps maintain the "power" of the color
      $fg: scale-color($fg, $lightness: $step, $saturation: $step/2);
    }
    @return $fg;
  }
}

El archivo final de Sass

¡Aquí está el conjunto completo de funciones! Abra esto en CodePen para editar las variables de color en la parte superior del archivo y vea los ajustes que realiza el Sass:

Todas las funciones auxiliares están allí, así como la tabla de búsqueda de 256 líneas. Muchos comentarios deberían ayudar a la gente a comprender lo que está sucediendo.

Cuando se ha encontrado un caso de borde, una versión en SassMeister con salida de depuración fue útil mientras lo desarrollaba para ver qué podría estar sucediendo. (Cambié la función principal a un mixin para poder depurar la salida). Siéntase libre de hurgar en esto también.

Juega con esta esencia en SassMeister.

Y finalmente, las funciones se eliminaron de CodePen y se colocaron en un repositorio de GitHub. Deje los problemas en la cola si tiene problemas.

¡Código genial! ¿Pero puedo usar esto en producción?

Quizás.

Me gustaría decir que sí, pero he estado iterando sobre este espinoso problema por un tiempo. Me siento confiado en este código, pero me encantaría recibir más información. Úselo en un proyecto pequeño y patee los neumáticos. Déjame saber cómo funciona el tiempo de construcción. Avíseme si se encuentra con casos extremos en los que no se proporcionan valores de color de paso. Envíe los problemas al repositorio de GutHub. Sugiera mejoras basadas en otro código que haya visto en la naturaleza.

Me encantaría decir que he automatizado todas las cosas interesantes, pero también sé que debe probarse en la carretera antes de que pueda llamarse Production Ready ™. Estoy emocionado de presentarlo al mundo. Gracias por leer y espero saber muy pronto cómo lo está utilizando.