El otro día, Florens Verschelde preguntó sobre la definición de estilos de modo oscuro para una clase y una consulta de medios, sin repetir las declaraciones de propiedades personalizadas de CSS. Me había encontrado con este problema en el pasado pero no había encontrado una solución adecuada.
Lo que queremos es evitar redefinir, y por lo tanto repetir, propiedades personalizadas al cambiar entre los modos claro y oscuro. Ese es el objetivo de la programación DRY (Don’t Repeat Yourself), pero el patrón típico para cambiar de tema suele ser algo como esto:
:root {
--background: #fff;
--text-color: #0f1031;
/* etc. */
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f1031;
--text-color: #fff;
/* etc. */
}
}
¿Ves lo que quiero decir? Claro, puede que no parezca gran cosa en un ejemplo abreviado como este, pero imagina hacer malabares con docenas de propiedades personalizadas a la vez, ¡eso es mucha duplicación!
Entonces recordé el truco de Lea Verou usando --var: ;
, y aunque no me di cuenta al principio, encontré una manera de hacerlo funcionar: no con var(--light-value, var(--dark-value))
o alguna combinación anidada como esa, ¡pero usando ambos uno al lado del otro!
Ciertamente, alguien más inteligente debe haber descubierto esto antes que yo, pero no he oído hablar de aprovechar (o más bien abusar) de las propiedades personalizadas de CSS para lograrlo. Sin más preámbulos, aquí está la idea:
--color: var(--light, orchid) var(--dark, rebeccapurple);
Si el --light
el valor se establece en initial
, se utilizará el respaldo (orchid
), lo que significa --dark
debe establecerse en un carácter de espacio en blanco (que es un valor válido), haciendo que el valor calculado final se vea así:
--color: orchid ; /* Note the additional whitespace */
Por el contrario, si --light
se establece en un espacio en blanco y --dark
a initial
, terminamos con un valor calculado de:
--color: rebeccapurple; /* Again, note the whitespace */
Ahora, esto es genial, pero necesitamos definir el --light
y --dark
propiedades personalizadas, basadas en el contexto. El usuario puede tener una preferencia de sistema establecida (ya sea clara u oscura), o puede haber cambiado el tema del sitio web con algún elemento de la interfaz de usuario. Al igual que en el ejemplo de Florens, definiremos estos tres casos, con algunas mejoras menores en la legibilidad que Lea propuso usando constantes “on” y “off” para que sea más fácil de entender de un vistazo:
:root {
/* Thanks Lea Verou! */
--ON: initial;
--OFF: ;
}
/* Light theme is on by default */
.theme-default,
.theme-light {
--light: var(--ON);
--dark: var(--OFF);
}
/* Dark theme is off by default */
.theme-dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* If user prefers dark, then that's what they'll get */
@media (prefers-color-scheme: dark) {
.theme-default {
--light: var(--OFF);
--dark: var(--ON);
}
}
Luego podemos configurar todas nuestras variables de tema en una sola declaración, sin repetición. En este ejemplo, el theme-*
las clases se establecen en el html
elemento, por lo que podemos usar :root
como selector, como a muchas personas les gusta hacer, pero puede establecerlos en el body
, si la naturaleza en cascada de las propiedades personalizadas tiene más sentido de esa manera:
:root {
--text: var(--light, black) var(--dark, white);
--bg: var(--light, orchid) var(--dark, rebeccapurple);
}
Y para usarlos usamos var()
con alternativas integradas, porque nos gusta tener cuidado:
body {
color: var(--text, navy);
background-color: var(--bg, lightgray);
}
Con suerte, ya estás empezando a ver los beneficios aquí. En lugar de definir y cambiar cargas de propiedades personalizadas, estamos tratando con dos y configurando todas las demás solo una vez. :root
. Esa es una gran mejora desde donde comenzamos.
Incluso DRYer con preprocesadores
Si me mostraras esta siguiente línea de código fuera de contexto, ciertamente estaría confundido porque un color es un valor único, ¡no dos!
--text: var(--light, black) var(--dark, white);
Por eso prefiero abstraer un poco las cosas. Podemos configurar una función con nuestro preprocesador favorito, que es Sass en mi caso. Si mantenemos nuestro código arriba definiendo nuestro --light
y --dark
valores en varios contextos, necesitamos realizar un cambio solo en la declaración de propiedad personalizada real. Vamos a crear un light-dark
función que nos devuelve la sintaxis CSS:
@function light-dark($light, $dark) {
@return var(--light, #{ $light }) var(--dark, #{ $dark });
}
Y lo usaríamos así:
:root {
--text: #{ light-dark(black, white) };
--bg: #{ light-dark(orchid, rebeccapurple) };
--accent: #{ light-dark(#6d386b, #b399cc) };
}
Notarás que hay delimitadores de interpolación #{ … }
alrededor de la llamada a la función. Sin estos, Sass generaría el código tal cual (como una función CSS de vainilla). Puede jugar con varias implementaciones de esto, pero la complejidad de la sintaxis depende de sus gustos.
¿Qué tal eso para una base de código mucho DRYer?
¿Más de un tema? ¡No hay problema!
Potencialmente, podría hacer esto con más de dos modos. Cuantos más temas agregue, más complejo se volverá de administrar, ¡pero el punto es que es posible! Agregamos otro conjunto de temas de ON
o OFF
variables y establezca una variable adicional en la lista de valores.
.theme-pride {
--light: var(--OFF);
--dark: var(--OFF);
--pride: var(--ON);
}
:root {
--text:
var(--light, black)
var(--dark, white)
var(--pride, #ff8c00)
; /* Line breaks are absolutely valid */
/* Other variables to declare… */
}
¿Es esto hacky? Sí, absolutamente lo es. ¿Es este un gran caso de uso para booleanos CSS potenciales que aún no existen? Bueno, ese es el sueño.
¿Y usted? ¿Es esto algo que ha descubierto con un enfoque diferente? Compártelo en los comentarios!