Uso de “pilas” de propiedades personalizadas para controlar la cascada
Desde el inicio de CSS en 1994, la cascada y la herencia han definido cómo diseñamos en la web. Ambas son características poderosas pero, como autores, hemos tenido muy poco control sobre cómo interactúan. La especificidad del selector y el orden de las fuentes proporcionan un control mínimo de "capas", sin muchos matices, y la herencia requiere un linaje ininterrumpido. Ahora, las propiedades personalizadas de CSS nos permiten gestionar y controlar tanto la cascada como la herencia de nuevas formas.
Quiero mostrarles cómo he utilizado “pilas” de propiedades personalizadas para resolver algunos de los problemas comunes que enfrentan las personas en la cascada: desde estilos de componentes con alcance hasta capas de intenciones más explícitas.
Una introducción rápida a las propiedades personalizadas
De la misma manera que los navegadores han definido nuevas propiedades usando un prefijo de proveedor como -webkit-
o -moz-
, podemos definir nuestras propias propiedades personalizadas con un --
prefijo "vacío". Al igual que las variables en Sass o JavaScript, podemos usarlas para nombrar, almacenar y recuperar valores, pero al igual que otras propiedades en CSS, caen en cascada y heredan con el DOM.
/* Define a custom property */html { --brand-color: rebeccapurple;}
Para acceder a esos valores capturados, utilizamos la var()
función. Tiene dos partes: primero el nombre de nuestra propiedad personalizada y luego un respaldo en caso de que esa propiedad no esté definida:
button { /* use the --brand-color if available, or fall back to deeppink */ background: var(--brand-color, deeppink);}
Esto no es una alternativa de soporte para navegadores antiguos. Si un navegador no comprende las propiedades personalizadas, ignorará toda la var()
declaración. En cambio, esta es una forma integrada de manejar variables no definidas, similar a una pila de fuentes que define familias de fuentes alternativas cuando una no está disponible. Si no proporcionamos un recurso alternativo, el valor predeterminado es unset
.
Construyendo “pilas” de variables
Esta capacidad de definir un recurso alternativo es similar a las "pilas de fuentes" utilizadas en la font-family
propiedad. Si la primera familia no está disponible, se utilizará la segunda y así sucesivamente. La var()
función solo acepta un único respaldo, pero podemos anidar var()
funciones para crear “pilas” de respaldo de propiedades personalizadas de cualquier tamaño:
button { /* try Consolas, then Menlo, then Monaco, and finally monospace */ font-family: Consolas, Menlo, Monaco, monospace; /* try --state, then --button-color, then --brand-color, and finally deeppink */ background: var(--state, var(--button-color, var(--brand-color, deeppink)));}
Si esa sintaxis anidada para propiedades apiladas parece voluminosa, puede usar un preprocesador como Sass para hacerla más compacta.
Esa limitación de respaldo único es necesaria para admitir respaldos con una coma dentro, como pilas de fuentes o imágenes de fondo en capas:
html { /* The fallback value is "Helvetica, Arial, sans-serif" */ font-family: var(--my-font, Helvetica, Arial, sans-serif);}
Definición de “alcance”
Los selectores de CSS nos permiten profundizar en el árbol HTML DOM y aplicar estilo a elementos en cualquier lugar de la página o elementos en un contexto anidado particular.
/* all links */a { color: slateblue; }/* only links inside a section */section a { color: rebeccapurple; }/* only links inside an article */article a { color: deeppink; }
Esto es útil, pero no captura la realidad de los estilos “modulares” orientados a objetos o basados en componentes. Es posible que tengamos varios artículos y apartes, anidados en varias configuraciones. Necesitamos una forma de aclarar qué contexto o alcance debe tener prioridad cuando se superponen.
Ámbitos de proximidad
Digamos que tenemos un .light
tema y un .dark
tema. Podemos usar esas clases en el html
elemento raíz para definir un valor predeterminado para toda la página, pero también podemos aplicarlas a componentes específicos, anidados de varias maneras:
Cada vez que aplicamos una de nuestras clases de modo de color, las propiedades background
y color
se restablecen y luego se heredan mediante encabezados y párrafos anidados. En nuestro contexto principal, los colores heredan de la .light
clase, mientras que el encabezado y el párrafo anidados heredan de la .dark
clase. La herencia se basa en el linaje directo, por lo que tendrá prioridad el antepasado más cercano con un valor definido. A eso lo llamamos proximidad .
La proximidad es importante para la herencia, pero no tiene ningún impacto en los selectores, que dependen de la especificidad. Eso se convierte en un problema si queremos darle estilo a algo dentro de los contenedores oscuros o claros.
Aquí he intentado definir variantes de botones claros y oscuros. Los botones del modo claro deben tener rebeccapurple
texto white
para que se destaquen, y los botones del modo oscuro deben tener plum
texto black
. Estamos seleccionando los botones directamente en función de un contexto claro y oscuro, pero no funciona:
Algunos de los botones están en ambos contextos, con ambos .light
y .dark
ancestros. Lo que queremos en ese caso es que el tema más cercano tome el control (comportamiento de proximidad de herencia), pero lo que obtenemos es que el segundo selector anule al primero (comportamiento en cascada). Dado que los dos selectores tienen la misma especificidad, el orden de origen determina el ganador.
Propiedades personalizadas y proximidad
Lo que necesitamos aquí es una forma de heredar estas propiedades del tema, pero solo aplicarlas a elementos secundarios específicos. ¡Las propiedades personalizadas lo hacen posible! Podemos definir valores en los contenedores claros y oscuros, mientras solo usamos sus valores heredados en elementos anidados, como nuestros botones.
Comenzaremos configurando los botones para usar propiedades personalizadas, con un valor alternativo "predeterminado", en caso de que esas propiedades no estén definidas:
button { background: var(--btn-color, rebeccapurple); color: var(--btn-contrast, white);}
Ahora podemos establecer esos valores según el contexto y se aplicarán al ancestro apropiado según la proximidad y la herencia:
.dark { --btn-color: plum; --btn-contrast: black;}.light { --btn-color: rebeccapurple; --btn-contrast: white;}
Como ventaja adicional, utilizamos menos código en general y una button
definición unificada:
Pienso en esto como crear una API de parámetros disponibles para el componente del botón. Sara Soueidan y Lea Verou han cubierto bien este tema en artículos recientes.
Propiedad de los componentes
A veces la proximidad no es suficiente para definir el alcance. Cuando los marcos de JavaScript generan "estilos de alcance", están estableciendo una propiedad específica del elemento objeto . Un componente de "diseño de pestañas" posee las pestañas en sí, pero no el contenido detrás de cada pestaña. Esto es también lo que la convención BEM intenta capturar en .block__element
nombres de clases complejos.
Nicole Sullivan acuñó el término “alcance del donut” para hablar de este problema en 2011. Si bien estoy seguro de que tiene ideas más recientes sobre el tema, el problema fundamental no ha cambiado. Los selectores y la especificidad son excelentes para describir cómo construimos estilos detallados sobre patrones amplios, pero no transmiten un sentido claro de propiedad.
Podemos utilizar pilas de propiedades personalizadas para ayudar a resolver este problema. Comenzaremos creando propiedades "globales" en el html
elemento que son para nuestros colores predeterminados:
html { --background--global: white; --color--global: black; --btn-color--global: rebeccapurple; --btn-contrast--global: white;}
Ese tema global predeterminado ahora está disponible en cualquier lugar al que queramos hacer referencia a él. Lo haremos con un data-theme
atributo que aplica nuestros colores de primer plano y de fondo. Queremos que los valores globales proporcionen un respaldo predeterminado, pero también queremos la opción de anular con un tema específico. Ahí es donde entran las "pilas":
[data-theme] { /* If there's no component value, use the global value */ background: var(--background--component, var(--background--global)); color: var(--color--component, var(--color--global));}
Ahora podemos definir un componente invertido configurando las *--component
propiedades como lo contrario de las propiedades globales:
[data-theme='invert'] { --background--component: var(--color--global); --color--component: var(--background--global);}
Pero no queremos que esas configuraciones se hereden más allá del donut de propiedad, por lo que restablecemos esos valores a initial
(indefinido) en cada tema. Querremos hacer esto con una especificidad más baja, o antes en el orden de origen, para que proporcione un valor predeterminado que cada tema pueda anular:
[data-theme] { --background--component: initial; --color--component: initial;}
La initial
palabra clave tiene un significado especial cuando se utiliza en propiedades personalizadas, revirtiéndolas a un estado Garantizado-No válido. Eso significa que, en lugar de pasarse a set background: initial
or color: initial
, la propiedad personalizada se convierte en undefined
, y volvemos al siguiente valor de nuestra pila, la configuración global.
Podemos hacer lo mismo con nuestros botones y luego asegurarnos de aplicarlo data-theme
a cada componente. Si no se proporciona ningún tema específico, cada componente utilizará de forma predeterminada el tema global:
Definición de “orígenes”
La cascada CSS es una serie de capas de filtrado que se utilizan para determinar qué valor debe tener prioridad cuando se definen varios valores en la misma propiedad. La mayoría de las veces interactuamos con las capas de especificidad , o las capas finales basadas en el orden de origen, pero la primera capa de la cascada es el "origen" de un estilo. El origen describe de dónde viene un estilo: a menudo, el navegador (valores predeterminados), el usuario (preferencias) o el autor (somos nosotros).
De forma predeterminada, los estilos de autor anulan las preferencias del usuario, que a su vez anulan los valores predeterminados del navegador. Eso cambia cuando alguien aplica `!important` a un estilo, y los orígenes se invierten: los estilos `!important` del navegador tienen el origen más alto, luego las preferencias importantes del usuario, luego los estilos importantes de nuestro autor, sobre todas las capas normales. Hay algunos orígenes adicionales, pero no los analizaremos aquí.
Cuando creamos “pilas” de propiedades personalizadas, construimos un comportamiento muy similar. Si quisiéramos representar los orígenes existentes como una pila de propiedades personalizadas, se vería así:
.origins-as-custom-properties { color: var(--browser-important, var(--user-important, var(--author-important, var(--author, var(--user, var(--browser))))));}
Esas capas ya existen, por lo que no hay motivo para recrearlas. Pero estamos haciendo algo muy similar cuando colocamos capas de nuestros estilos "global" y "componente" arriba: creando una capa de origen de "componente" que anula nuestra capa "global". Ese mismo enfoque se puede utilizar para resolver varios problemas de capas en CSS, que no siempre pueden describirse mediante especificidad:
- Anular » Componente » Tema » Predeterminado
- Tema » Sistema o framework de diseño
- Estado » Tipo » Predeterminado
Veamos algunos botones nuevamente. Necesitaremos un estilo de botón predeterminado, un estado deshabilitado y varios "tipos" de botones como danger
y . No queremos que el estado anule siempre las variaciones de tipo, pero los selectores no capturan esa distinción:primary
secondary
disabled
Pero podemos definir una pila que proporcione capas de "tipo" y "estado" en el orden en que queremos que se prioricen:
button { background: var(--btn-state, var(--btn-type, var(--btn-default)));}
Ahora, cuando configuramos ambas variables, el estado siempre tendrá prioridad:
He utilizado esta técnica para crear un marco de colores en cascada que permite temas personalizados basados en capas:
- Atributos de tema predefinidos en HTML
- Preferencias de color del usuario
- Modos claro y oscuro
- Valores predeterminados del tema global
Mezclar y combinar
Estos enfoques se pueden llevar al extremo, pero la mayoría de los casos de uso cotidianos se pueden manejar con dos o tres valores en una pila, a menudo usando una combinación de las técnicas anteriores:
- Una pila variable para definir las capas.
- Herencia para configurarlos en función de la proximidad y el alcance.
- Aplicación cuidadosa del valor "inicial" para eliminar elementos anidados de un alcance
Hemos estado utilizando estas "pilas" de propiedades personalizadas en nuestros proyectos en OddBird. Todavía estamos descubriendo a medida que avanzamos, pero ya han sido útiles para resolver problemas que eran difíciles usando solo selectores y especificidad. Con las propiedades personalizadas, no tenemos que luchar contra la cascada o la herencia. Podemos capturarlos y aprovecharlos, según lo previsto, con más control sobre cómo deben aplicarse en cada caso. Para mí, eso es una gran victoria para CSS, especialmente cuando se desarrollan marcos, herramientas y sistemas de estilo.
Deja un comentario