Cuando empecé a usar oklch en mis proyectos pensé que el problema del contraste estaba resuelto: oklch tiene un componente L (lightness) perceptual, así que dos colores con la misma L se ven igual de claros. Spoiler: no es así de simple. WCAG AA usa una métrica de contraste basada en sRGB, no en lightness percibida. Esta es la metodología que uso ahora.
Por qué oklch en primer lugar
HSL es traicionero. Dos colores con la misma L pueden verse muy distintos al ojo: hsl(60 100% 50%) (amarillo) se ve mucho más claro que hsl(240 100% 50%) (azul). Es porque HSL no respeta cómo el ojo humano percibe el brillo.
Oklch sí. Cuando dices oklch(76% 0.230 132) y oklch(76% 0.230 240), ambos se ven igual de "claros" perceptualmente. Eso me permite construir una paleta donde el accent verde, el secondary violeta y los grises tienen "pesos" coherentes en cualquier hue.
Pero — y es un PERO importante — el contraste WCAG no se mide en oklch. Se mide en sRGB con la fórmula de luminancia relativa de WCAG 2.1. Eso significa que puedes tener dos colores con misma L perceptual y contraste WCAG completamente distinto.
El problema concreto que enfrenté
En la paleta de VantLabs, el accent es oklch(76% 0.230 132) — un lime brillante. Mi instinto fue que cualquier texto blanco (#ffffff) sobre ese lime debería ser legible: un fondo "claro" con texto "más claro" debería contrastar como cualquier botón de Tailwind con bg-blue-500.
Pero el lime al 76% L tiene una luminancia muy alta en sRGB. Texto blanco sobre ese lime da un contraste de ~1.8:1 — muy por debajo de los 4.5:1 que pide WCAG AA para texto normal. El texto desaparece. La solución es usar texto oscuro (casi negro) sobre el lime — contraste ~14:1, perfecto.
La regla que aprendí: los colores high-chroma con L 70-85% son trampas. Se ven brillantes pero su luminancia sRGB no es la que tu intuición espera. Texto blanco les rebota; texto oscuro los aterriza.
Mi flujo de verificación
Tres pasos, todos manuales pero rápidos:
- Generar la paleta en oklch. Decido los hues, defino la escala 50→950 con L progresiva (97% → 8%) y chroma máxima en el medio (500). Esto vive en
tokens.css. - Convertir cada par crítico a sRGB y verificar contraste. Uso oklch.com para convertir y WebAIM Contrast Checker para verificar. Los pares críticos son: texto-sobre-bg-base, texto-sobre-bg-surface, texto-sobre-accent-fill, texto-secundario-sobre-bg-base.
- Validar dark mode aparte. Las mismas pruebas pero con los semantic tokens del dark theme. WCAG es independiente del modo, pero los pares cambian completamente al invertir.
En total, cada vez que ajusto la paleta, el ciclo de verificación me toma ~15 minutos. Es manual pero confiable.
Lo que automatizo
Tengo un script chico que toma tokens.css, parsea las CSS variables oklch, las convierte a sRGB usando culori (una librería JS de color science), y calcula el contraste WCAG entre los pares críticos. Si alguno cae bajo 4.5:1 (texto normal) o 3:1 (texto large), el script falla con código de salida 1. Lo corro como parte de los checks pre-commit.
Es 80 líneas de código y me ha salvado de shippear dos paletas problemáticas. Cuando algún día limpie el código, lo abro en GitHub como utility chiquita.
Los focus rings, el detalle más olvidado
La mayoría de los devs verifican contraste de texto y se olvidan de los focus rings. WCAG 2.1 pide que el indicador de foco tenga al menos 3:1 de contraste con el background adyacente (no con el componente, sino con el espacio detrás). Esto significa que un focus ring verde lime sobre un bg blanco probablemente NO pasa.
La solución en VantLabs: el focus ring es oklch(56% 0.190 130) (volt-700), no el accent-500. Es más oscuro, mantiene la identidad del color, y pasa contraste sobre cualquier background del sitio. Visualmente sigue siendo "el verde de la marca", solo que la versión que sirve para focus.
Lo que sigo aprendiendo
APCA (Accessible Perceptual Contrast Algorithm) es la propuesta de WCAG 3.0 para reemplazar el cálculo actual de contraste. Es matemáticamente más correcto, especialmente para colores high-chroma como los míos. Aún no es estándar, pero ya hay tooling y va a ser el futuro. Mi plan: cuando WCAG 3 sea estable, migrar mi script a APCA y reverificar todo. Por ahora, AA en sRGB sigue siendo el piso.