accent-color y color-scheme: teñir sin destruir
Las dos propiedades que estilan controles nativos sin apagar su dibujo, qué controles alcanzan exactamente, y por qué color-scheme hace mucho más de lo que su nombre sugiere.
Antes de reconstruir un control conviene agotar lo que la plataforma te deja cambiar sin apagar nada. Son dos propiedades, se escriben en tres líneas y resuelven el ochenta por ciento de los casos reales: un formulario que respeta la identidad de marca y se adapta al modo oscuro sin que hayas tocado un solo pseudo-elemento. La trampa es que su alcance es limitado y está mal documentado en la cabeza de la mayoría de la gente, así que acabas creyendo que no funcionan cuando lo que pasa es que las estás aplicando donde no llegan.
- Enumerar los controles exactos que responden a
accent-colory los que no. - Explicar por qué
color-schemeafecta a los controles, las barras de scroll y el lienzo por defecto. - Combinar ambas para un formulario que funcione en claro y oscuro sin duplicar reglas.
- Detectar cuándo el contraste del color de acento deja de ser aceptable.
accent-color: el color de acento del usuario, sobrescrito
accent-color sustituye el color con el que el motor pinta la parte “activa” de un control nativo. Es heredable, lo que significa que declararla una vez en :root tiñe el formulario entero, y es de las pocas propiedades que atraviesan la frontera del árbol de sombra de agente de usuario sin que tengas que apagar appearance.
Los controles que responden son cuatro, y solo cuatro:
| Control | Qué se tiñe |
|---|---|
input[type="checkbox"] |
el relleno cuando está marcada |
input[type="radio"] |
el punto interior |
input[type="range"] |
el pulgar y, en algunos motores, la parte recorrida de la pista |
progress |
la barra de progreso |
No afecta a select, ni a los campos de texto, ni al cursor de escritura, ni al color de selección de texto, ni a meter. Para el cursor tienes caret-color. Para la selección, el pseudo-elemento ::selection. Para meter, nada estándar: el motor decide el color en función del valor y los umbrales, que es precisamente para lo que sirve meter.
:root {
accent-color: oklch(0.58 0.19 265);
caret-color: oklch(0.58 0.19 265);
}
::selection {
background: oklch(0.58 0.19 265 / 0.28);
}
Tres declaraciones y el formulario entero deja de ser azul de sistema. Nada se ha roto, ningún estado ha desaparecido, el modo de contraste forzado sigue mandando por encima, y el día que un motor mejore cómo dibuja el pulgar del range te beneficias gratis.
El valor por defecto es auto, que significa “usa el color de acento que el usuario haya configurado en su sistema operativo”. Merece la pena pararse un segundo en eso: el valor por defecto ya es una personalización del usuario, y al escribir accent-color la estás anulando. Es una anulación razonable en la mayoría de productos, pero no es neutra.
Los motores aplican una comprobación de contraste sobre el color que declaras. Si tu acento es demasiado claro, el motor puede oscurecer la marca de verificación de la casilla para que siga siendo legible, en lugar de dibujarla blanca sobre casi blanco. No es un bug: está en la especificación de accent-color, que permite al agente de usuario elegir un color de primer plano que contraste. Significa que no puedes predecir con exactitud el color de la marca, así que si el diseño depende de ello a nivel de píxel, ese es el momento de reconstruir el control.
color-scheme: mucho más que el fondo
color-scheme es la propiedad peor entendida del CSS moderno porque su nombre sugiere que cambia colores. Lo que hace realmente es declarar en qué esquemas de color sabe funcionar tu documento, y a partir de esa declaración el agente de usuario cambia todo lo que dibuja él.
:root {
color-scheme: light dark;
}
Con esa línea, y sin ninguna otra regla, cambian cinco cosas cuando el usuario tiene el sistema en oscuro:
- El lienzo por defecto. El fondo del documento pasa a oscuro y
colorpasa a claro. Es el fondo del navegador, no unbackgroundtuyo, así que no hay ningún destello blanco antes de que cargue tu hoja de estilos. - Los controles de formulario. Casillas, radios, campos de texto,
select,range, todos se pintan con el widget oscuro de la plataforma. Esto es lo grande: sincolor-scheme, un formulario en una página oscura se ve como un recorte blanco pegado encima. - Las barras de scroll. Pasan a la variante oscura del sistema.
- Otras superficies de agente de usuario: el selector de fecha, el desplegable nativo del
select, el diálogo de selección de archivo en algunos casos. - La función
light-dark(). Sin uncolor-schemedeclarado en el elemento o heredado,light-dark()no tiene contra qué resolver y devuelve siempre la rama clara.
El orden de los valores no es decorativo: light dark significa “prefiero claro, sé hacer oscuro”; dark light significa “prefiero oscuro, sé hacer claro”. Cuando el usuario no ha expresado preferencia, el motor toma el primero.
color-scheme también es aplicable a elementos concretos, y ahí está su uso más interesante. Un panel que quieres oscuro dentro de una página clara:
.panel-consola {
color-scheme: dark;
background: Canvas;
color: CanvasText;
}
Los controles de formulario dentro de ese panel se pintarán oscuros aunque el resto de la página sea clara, porque color-scheme se hereda y el motor consulta el valor calculado del propio control. Canvas y CanvasText son colores de sistema estándar que siguen el esquema activo; usarlos en lugar de valores fijos es lo que hace que ese panel también funcione bajo contraste forzado.
Los dos juntos, y sus límites
El patrón mínimo completo para un formulario que se comporta en ambos esquemas cabe en un bloque:
:root {
color-scheme: light dark;
accent-color: light-dark(oklch(0.52 0.18 265), oklch(0.72 0.15 265));
caret-color: currentColor;
}
input, select, textarea, button {
font: inherit;
color: inherit;
}
Fíjate en light-dark() sobre accent-color: el mismo tono en claro y en oscuro tiene contrastes muy distintos contra el fondo, y el acento oscuro que funciona sobre blanco casi siempre queda apagado sobre negro. Subir la claridad y bajar el croma en la rama oscura es el ajuste correcto, y en el nivel de sistemas de diseño se explica por qué esa asimetría es perceptual y no un capricho.
La línea font: inherit es el otro clásico: los controles no heredan la tipografía por defecto, la reciben del sistema. Es una de las poquísimas cosas que conviene resetear siempre.
Los límites de este enfoque son claros y hay que conocerlos antes de invertir en él. No puedes cambiar el radio de una casilla, ni el grosor de la pista del range, ni el icono de la flecha del select, ni el espaciado interno del desplegable. Si el diseño exige cualquiera de esas cosas, accent-color no te va a llevar allí y el siguiente escalón es apagar el dibujo, con el coste que eso tiene según la lección anterior.
Hay un detalle que solo se aprende viendo una grabación de pantalla a cámara lenta, y que ninguna cantidad de CSS de modo oscuro resuelve. Cuando el navegador empieza a pintar un documento, el lienzo por defecto es blanco. Tu body { background: #111 } no se aplica hasta que la hoja de estilos ha llegado, se ha parseado y el primer paint la incluye. En una conexión lenta, o en una navegación entre documentos, eso son entre cincuenta y varios cientos de milisegundos de destello blanco a pantalla completa que en modo oscuro resulta físicamente doloroso. color-scheme es distinto: el navegador lo lee de la meta etiqueta name="color-scheme" en la cabecera del HTML, antes de cualquier hoja de estilos, y cambia el color del lienzo antes del primer paint. Por eso la recomendación seria no es solo declararlo en CSS, sino duplicarlo en el <head> con <meta name="color-scheme" content="light dark">. Y por eso el mismo problema reaparece en cuanto sirves el modo oscuro desde una clase en html puesta por JavaScript: la clase llega después del primer paint, y vuelves a tener el destello. La única arquitectura de tema que no parpadea es la que deja que color-scheme y prefers-color-scheme hagan el trabajo por defecto, y reserva el JavaScript exclusivamente para el caso en que el usuario ha elegido explícitamente ir contra su sistema.