Convertidor de colores HEX, RGB, HSL y OKLCH, con contraste WCAG

Este convertidor de colores pasa cualquier color entre HEX, RGB, HSL, OKLCH y los nombres de CSS, como steelblue. Debajo, el comprobador de contraste te dice si un color de texto se lee bien sobre un fondo según las pautas de accesibilidad WCAG 2.2, en los niveles AA y AAA. Todo se calcula en tu navegador: no se envía nada.

Ejemplos:

El contraste es el mismo en los dos sentidos: el cociente no cambia si intercambias texto y fondo. Lo que sí cambia es cómo se ve: texto oscuro sobre fondo claro suele leerse mejor en párrafos largos.

Los formatos de color

  • HEX: #1e64af son tres pares de cifras hexadecimales, de 00 a ff, para el rojo, el verde y el azul. #16a es la forma corta de #1166aa. Si escribes 8 cifras, las dos últimas son la transparencia, que el convertidor ignora.
  • RGB: rgb(30 100 175) son los mismos tres canales en decimal, de 0 a 255. CSS acepta también la forma antigua con comas, rgb(30, 100, 175).
  • HSL: hsl(211 70.7% 40.2%) es el tono en grados (0 rojo, 120 verde, 240 azul), la saturación y la luminosidad. Es cómodo para hacer variantes de un color, pero su luminosidad engaña: un amarillo y un azul con 50% no parecen igual de claros.
  • OKLCH: oklch(50.12% 0.1372 253.67) es la luminosidad percibida, el croma (la intensidad) y el tono. Está pensado para que dos colores con la misma L se vean igual de claros, por eso sirve para crear paletas y degradados uniformes. Los navegadores actuales lo aceptan en CSS. Puede describir colores más vivos de los que muestra una pantalla sRGB: si escribes uno así, el convertidor lo recorta al más cercano y te avisa.

Redondeo y precisión

HSL se muestra con un decimal: con esa precisión, los 16,777,216 colores de 8 bits vuelven exactamente al mismo HEX si copias el valor y lo conviertes de nuevo. Con HSL en números enteros, como lo muestran muchas herramientas, cambiarían casi nueve de cada diez (el 89.4 %) en al menos un canal. OKLCH se muestra con dos decimales en la luminosidad y el tono y cuatro en el croma: así vuelven igual todos menos 13,617 colores (el 0.08 %), en los que algún canal cambia en una unidad, una diferencia que no se ve. Las fórmulas son las de la especificación CSS Color 4.

Cómo se calcula el contraste

  • Luminancia relativa: cada canal se pasa a un valor de 0 a 1 (se divide entre 255) y se linealiza: si es 0.04045 o menos, se divide entre 12.92; si no, se calcula ((c + 0.055) / 1.055)2.4. Después, L = 0.2126 R + 0.7152 G + 0.0722 B. El verde pesa más porque el ojo es más sensible a él. Las versiones de WCAG anteriores a mayo de 2021 decían 0.03928 en lugar de 0.04045: con colores de 8 bits el resultado es el mismo.
  • Contraste: (L1 + 0.05) / (L2 + 0.05), con L1 la luminancia del color más claro. Va de 1:1 (dos colores iguales) a 21:1 (negro sobre blanco).
  • Sin redondear hacia arriba: 4.499:1 no cumple el mínimo de 4.5:1. Por eso la herramienta muestra el cociente truncado a dos decimales y compara el valor exacto. El gris #777777 sobre blanco da 4.478:1 y no llega.

Los niveles de WCAG 2.2

  • AA, texto normal (criterio 1.4.3): al menos 4.5:1. Es el nivel que exigen la mayoría de las leyes de accesibilidad.
  • AA, texto grande: al menos 3:1. Texto grande es el de 18 puntos o más (unos 24 px de CSS) o de 14 puntos en negrita (unos 18.5 px).
  • AAA (criterio 1.4.6): al menos 7:1 para texto normal y 4.5:1 para texto grande.
  • Iconos, bordes de campos y gráficos (criterio 1.4.11): al menos 3:1 contra los colores de al lado.

Convertir colores y medir el contraste en Python

El módulo colorsys de la biblioteca estándar convierte entre RGB y HSL (que llama HLS y devuelve en otro orden); el contraste se calcula con la fórmula de WCAG:

import colorsys


def hex_a_rgb(hex_):
    hex_ = hex_.lstrip("#")
    return tuple(int(hex_[i:i + 2], 16) for i in (0, 2, 4))


def rgb_a_hsl(r, g, b):
    h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)     # ojo: colorsys devuelve h, l, s
    return round(h * 360, 1), round(s * 100, 1), round(l * 100, 1)


def hsl_a_hex(h, s, l):
    r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100)
    return "#" + "".join(f"{round(c * 255):02x}" for c in (r, g, b))


def luminancia(r, g, b):
    """Luminancia relativa de WCAG 2.2 (umbral 0.04045)."""
    def lineal(c):
        c /= 255
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    return 0.2126 * lineal(r) + 0.7152 * lineal(g) + 0.0722 * lineal(b)


def contraste(color1, color2):
    l1, l2 = sorted((luminancia(*hex_a_rgb(color1)), luminancia(*hex_a_rgb(color2))), reverse=True)
    return (l1 + 0.05) / (l2 + 0.05)


azul = "#1e64af"
print(hex_a_rgb(azul), rgb_a_hsl(*hex_a_rgb(azul)))
print(hsl_a_hex(*rgb_a_hsl(*hex_a_rgb(azul))))
c = contraste(azul, "#ffffff")
print(f"{c:.2f}:1", "AA" if c >= 4.5 else "no AA", "AAA" if c >= 7 else "no AAA")
print(f'{contraste("#777777", "#ffffff"):.3f}:1')    # el gris que casi llega a 4.5
$ python3 colores.py(30, 100, 175) (211.0, 70.7, 40.2)
#1e64af
6.00:1 AA no AAA
4.478:1

Otras herramientas para desarrolladores: convertir una imagen a ASCII, el generador de UUID y el formateador de JSON. Todas, en decodigo.com/herramientas.

Luminancia relativa, contraste y criterios 1.4.3, 1.4.6 y 1.4.11: WCAG 2.2, recomendación del W3C del 12 de diciembre de 2024 (nota sobre 0.03928 y 0.04045 en la definición de luminancia relativa). Redondeo y equivalencias en píxeles: Understanding SC 1.4.3. Conversiones HSL y OKLCH, matrices de OKLab y los 148 nombres de color: CSS Color Module Level 4. La herramienta se comparó con colorsys de Python, con la fórmula de WCAG y con la biblioteca ColorAide (OKLCH) en 20,000 colores y 20,000 pares al azar, más 18,007 colores escritos en distintos formatos; la ida y vuelta se comprobó en los 16,777,216 colores (HSL, sin pérdida; OKLCH, 13,617 colores con algún canal que cambia en una unidad).