wandres.dev
FORMULARIOS · Estilar lo inestilizable

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Enumerar los controles exactos que responden a accent-color y los que no.
  • Explicar por qué color-scheme afecta 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.

💡
El contraste lo calcula el motor, y a veces te corrige

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:

  1. El lienzo por defecto. El fondo del documento pasa a oscuro y color pasa a claro. Es el fondo del navegador, no un background tuyo, así que no hay ningún destello blanco antes de que cargue tu hoja de estilos.
  2. 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: sin color-scheme, un formulario en una página oscura se ve como un recorte blanco pegado encima.
  3. Las barras de scroll. Pasan a la variante oscura del sistema.
  4. 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.
  5. La función light-dark(). Sin un color-scheme declarado 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.

color-scheme es la única forma de que no exista un fotograma blanco

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.