El modo oscuro no es una inversión
Las cuatro razones perceptuales por las que invertir la luminosidad produce una interfaz peor, y las reglas concretas de elevación, croma y contraste que sí funcionan.
Si construyes el modo oscuro invirtiendo la claridad de cada token, obtienes algo que técnicamente es oscuro y que se ve mal: los colores de marca chillan, las sombras desaparecen, la jerarquía se aplana y el texto blanco puro sobre negro puro produce un efecto de halo que cansa la vista en minutos. Ninguno de esos cuatro problemas es cuestión de gusto: los cuatro tienen una explicación perceptual concreta y una regla que los corrige. Esta lección es esa lista.
- Explicar por qué la elevación se invierte conceptualmente en el modo oscuro.
- Ajustar croma y claridad de los colores de marca para fondo oscuro.
- Evitar el halo del blanco puro sobre negro puro con valores concretos.
- Comprobar que el contraste sigue siendo correcto en los dos temas.
Primera: la elevación no se hereda del modo claro
En el modo claro, la profundidad se comunica con sombras: un elemento elevado proyecta una sombra sobre lo que hay debajo, exactamente como en el mundo físico con una fuente de luz cenital. Es una metáfora que funciona porque el fondo es claro y la sombra es más oscura que él, así que se ve.
Sobre un fondo oscuro esa metáfora se rompe. Una sombra oscura sobre un fondo oscuro es invisible: no hay contraste que la muestre. Subir la opacidad de la sombra no ayuda, solo produce una mancha negra alrededor del elemento.
La solución que usan los sistemas de diseño serios es cambiar el mecanismo: en el modo oscuro, cuanto más elevado está un elemento, más claro es su fondo. La metáfora deja de ser “proyecta sombra” y pasa a ser “está más cerca de la luz”. Funciona porque el ojo interpreta correctamente una superficie más clara sobre una más oscura como algo que está delante.
:root {
--superficie-0: light-dark(var(--gris-50), oklch(0.18 0.012 265));
--superficie-1: light-dark(white, oklch(0.23 0.013 265));
--superficie-2: light-dark(white, oklch(0.27 0.014 265));
--elevacion-1: light-dark(0 1px 3px oklch(0 0 0 / 0.1), none);
--elevacion-2: light-dark(0 4px 12px oklch(0 0 0 / 0.12), none);
}
Fíjate en dos detalles. Las superficies claras casi no cambian entre niveles —blanco sobre gris muy claro— porque ahí el trabajo lo hace la sombra. Y en oscuro la sombra desaparece del todo y el trabajo lo hace la claridad de la superficie. Son dos sistemas de elevación distintos, no dos valores del mismo.
Un recurso adicional en oscuro, muy útil para las superficies flotantes, es un borde de un píxel con blanco a baja opacidad, que simula el brillo del canto de un objeto iluminado:
--elevacion-flotante: light-dark(
0 8px 24px oklch(0 0 0 / 0.15),
0 0 0 1px oklch(1 0 0 / 0.09), 0 8px 24px oklch(0 0 0 / 0.4)
);
Segunda: el croma alto quema sobre fondo oscuro
Un color saturado sobre fondo claro se percibe como sólido. El mismo color sobre fondo oscuro se percibe como luminoso, y a partir de cierto croma produce halación: el ojo ve un halo difuso alrededor del borde, especialmente en azules y rojos intensos, y especialmente en pantallas OLED donde el negro es negro real.
La corrección tiene dos partes y OKLCH las hace triviales porque separa claridad de croma:
- Subir la claridad. Un color de marca pensado para contrastar sobre blanco es demasiado oscuro sobre negro. Necesita más luz.
- Bajar el croma. El mismo croma numérico se percibe como más intenso sobre fondo oscuro.
--color-accion: light-dark(
oklch(0.55 0.18 265),
oklch(0.72 0.13 265)
);
Claridad de 0.55 a 0.72, croma de 0.18 a 0.13, tono idéntico. Ese es el ajuste típico y funciona como punto de partida para casi cualquier tono. Conservar el tono exacto es lo que hace que siga pareciendo el mismo color de marca.
Si prefieres derivarlo en lugar de escribir los dos valores, los colores relativos lo hacen en una línea:
--color-accion-oscuro: oklch(from var(--color-accion) calc(l + 0.17) calc(c - 0.05) h);
Es elegante y tiene un riesgo: aplicado ciegamente a toda la paleta produce resultados raros en los extremos, porque calc(l + 0.17) sobre un color que ya tenía claridad 0.9 se sale del rango. Para una rampa completa suele salir mejor definir los valores.
El color corporativo está definido para papel y para fondo blanco. Sobre fondo oscuro suele fallar el contraste con el texto encima, o quemar, o las dos cosas. Esto genera una conversación incómoda con quien custodia la marca, y la forma de ganarla es enseñar el número: el mismo azul que da 4.8 de contraste sobre blanco puede dar 2.9 sobre el gris oscuro de tu superficie, y 2.9 no cumple. La marca no cambia; cambia el tono que se usa para texto y acción, que es una decisión de sistema y no de identidad.
Tercera: ni negro puro ni blanco puro
Texto blanco puro sobre negro puro es el máximo contraste posible y es una mala decisión. El motivo es óptico: en un contraste tan extremo, los bordes de las letras producen halación —el trazo blanco parece expandirse sobre el fondo— y el efecto es notablemente más fuerte en personas con astigmatismo, que son entre un tercio y la mitad de la población adulta. El texto parece borroso y desenfocado, y leerlo cansa.
Los valores que funcionan:
- Fondo: no
#000. Un gris muy oscuro con un punto del tono de tu paleta, alrededor deoklch(0.18 0.012 265). Además de evitar la halación, permite que existan superficies más oscuras que el fondo, que en negro puro sería imposible. - Texto principal: no blanco puro. Alrededor de
oklch(0.94 0.005 265), lo bastante claro para contrastar de sobra y lo bastante lejos del blanco para no vibrar. - Texto secundario: bajar claridad, no bajar opacidad. Volveremos a esto.
Hay una excepción real: las pantallas OLED apagan el píxel en negro puro y eso ahorra batería. Si tienes un modo de ahorro explícito, es un argumento legítimo. Para el modo oscuro por defecto, no compensa.
Cuarta: la opacidad no es una forma de atenuar
El atajo universal para el texto secundario es bajar la opacidad, y en modo oscuro se rompe por dos motivos.
El primero es que la opacidad depende de lo que haya detrás. Un texto al 60% sobre la superficie de nivel 0 y el mismo texto sobre la superficie de nivel 2 tienen contrastes distintos, porque el fondo es distinto. Con una jerarquía de elevación, el mismo token produce resultados diferentes según dónde se use, y algunos incumplen el mínimo.
El segundo es que la relación entre opacidad y claridad percibida no es lineal, así que el 60% que quedaba bien en claro no queda igual de bien en oscuro.
La solución es tener tokens de texto con claridad definida, no con opacidad:
--texto: light-dark(oklch(0.25 0.012 265), oklch(0.94 0.005 265));
--texto-tenue: light-dark(oklch(0.48 0.010 265), oklch(0.75 0.008 265));
--texto-sutil: light-dark(oklch(0.60 0.008 265), oklch(0.62 0.007 265));
Cada uno es un valor absoluto y su contraste contra cada superficie se puede medir una vez y darse por bueno. La opacidad queda para lo que sí es: transparencia real, superposiciones y estados deshabilitados.
La razón de fondo por la que la inversión automática nunca funciona, y la que conviene tener clara antes de discutir con nadie sobre esto, es que el modo claro y el modo oscuro no son la misma interfaz con otros colores: son dos entornos perceptuales distintos. En un entorno claro el ojo está adaptado a mucha luz, la pupila está cerrada, el contraste efectivo es alto y los colores saturados se leen como pigmento. En un entorno oscuro la pupila está abierta, el ojo es mucho más sensible, los colores saturados se leen como emisión de luz y los detalles finos se difuminan. Son las mismas condiciones que hacen que un cine se pinte de negro mate y una oficina de blanco: no es estética, es que el sistema visual funciona distinto en cada régimen. De ahí sale la única regla que de verdad hace falta memorizar, y es la que resuelve todos los casos que esta lección no cubre: lo que se conserva entre temas es la jerarquía, no los valores. Si en claro el título es más prominente que el cuerpo, en oscuro también debe serlo, pero la distancia numérica que produce esa prominencia será distinta. Si en claro la tarjeta se separa del fondo, en oscuro también, con otro mecanismo. Cuando revises un tema oscuro, no compares colores con el modo claro: compara relaciones. Y cuando alguien proponga un filtro de inversión para ahorrarse el trabajo, la respuesta más corta es que un filtro conserva los valores y destruye exactamente las relaciones, que es justo lo contrario de lo que hace falta.
Coge una pantalla de tu producto y haz dos capturas, en claro y en oscuro. Ordena en cada una los elementos por prominencia percibida —qué ves primero, segundo, tercero— sin mirar los valores. Si los dos órdenes no coinciden, tienes un fallo de tema aunque todos los contrastes cumplan. Después mide el contraste de cada token de texto contra cada superficie donde puede aparecer, no solo contra el fondo principal: ahí es donde aparecen los incumplimientos que nadie ve.