Cambio DRY con variables CSS: la diferencia de una declaración

Índice
  1. Por qué las variables CSS son útiles
  2. Cómo funciona la conmutación en el caso general

Esta es la primera publicación de una serie de dos partes que analiza la forma en que se pueden usar las variables CSS para que el código de interacciones y diseños complejos sea menos difícil de escribir y mucho más fácil de mantener. Esta primera entrega analiza varios casos de uso en los que se aplica esta técnica. La segunda publicación cubre el uso de valores de reserva y no válidos para extender la técnica a valores no numéricos.

¿Qué sucedería si le dijera que una sola declaración CSS marca la diferencia en la siguiente imagen entre el caso de pantalla ancha (izquierda) y el segundo (derecha)? ¿Y qué sucedería si le dijera que una sola declaración CSS marca la diferencia entre los elementos pares e impares en el caso de pantalla ancha?

¿O que una sola declaración CSS hace la diferencia entre los casos contraídos y expandidos a continuación?

¿Cómo es eso posible?

Bueno, como habrás adivinado por el título, todo está en el poder de las variables CSS.

Ya existen muchos artículos sobre qué son las variables CSS y cómo empezar a utilizarlas, por lo que no entraremos en eso aquí.

En lugar de eso, nos adentraremos directamente en por qué las variables CSS son útiles para lograr estos casos y otros, luego pasaremos a una explicación detallada de cómo hacerlo en varios casos. Codificaremos un ejemplo real desde cero, paso a paso, y, finalmente, obtendrás algo atractivo en forma de algunas demostraciones más que utilizan la misma técnica.

¡Entonces empecemos!

Por qué las variables CSS son útiles

Para mí, lo mejor de las variables CSS es que han abierto la puerta a diseñar cosas de una manera lógica, matemática y sin esfuerzo.

Un ejemplo de esto es la versión variable CSS del cargador Yin y Yang que codifiqué el año pasado. Para esta versión, creamos las dos mitades con los dos pseudoelementos del elemento cargador.

Usamos los mismos valores , y para las dos mitades. Todos estos valores dependen de una variable switch backgroundque border-colorinicialmente está establecida en en ambas mitades (los pseudoelementos), pero luego la cambiamos a para la segunda mitad (el pseudoelemento), modificando así dinámicamente los valores calculados de todas estas propiedades.transform-originanimation-delay--i01:after

Sin las variables CSS, tendríamos que configurar todas estas propiedades ( border-color, transform-origin, background, animation-delay) nuevamente en el :afterpseudoelemento y correr el riesgo de cometer algún error tipográfico o incluso olvidarnos de configurar algunas de ellas.

Cómo funciona la conmutación en el caso general

Cambiar entre un valor cero y un valor distinto de cero

En el caso particular del cargador yin y yang, todas las propiedades que cambiamos entre las dos mitades (pseudoelementos) van desde un valor cero para un estado del interruptor y un valor distinto de cero para el otro estado.

Si queremos que nuestro valor sea cero cuando el interruptor está apagado ( --i: 0) y distinto de cero cuando el interruptor está encendido ( --i: 1), entonces lo multiplicamos por el valor del interruptor ( var(--i)). De esta manera, si nuestro valor distinto de cero debería ser, digamos un valor angular de 30deg, tenemos:

  • Cuando el interruptor está apagado ( --i: 0), calc(var(--i)*30deg)calcula0*30deg = 0deg
  • cuando el interruptor está activado ( --i: 1), calc(var(--i)*30deg)calcula1*30deg = 30deg

Sin embargo, si queremos que nuestro valor sea distinto de cero cuando el interruptor esté apagado ( --i: 0) y cero cuando el interruptor esté encendido ( --i: 1), entonces lo multiplicamos por el complementario del valor del interruptor ( 1 - var(--i)). De esta manera, para el mismo valor angular distinto de cero de 30deg, tenemos:

  • Cuando el interruptor está apagado ( --i: 0), calc((1 - var(--i))*30deg)calcula(1 - 0)*30deg = 1*30deg = 30deg
  • cuando el interruptor está activado ( --i: 1), calc((1 - var(--i))*30deg)calcula(1 - 1)*30deg = 0*30deg = 0deg

Puedes ver este concepto ilustrado a continuación:

Para el caso particular del cargador, utilizamos valores HSL para border-colory background-color. HSL significa tono, saturación, luminosidad y se puede representar mejor visualmente con la ayuda de un bicono (que está formado por dos conos con las bases pegadas entre sí).

Las tonalidades van alrededor del bicono, siendo equivalente a 360°darnos un rojo en ambos casos.

La saturación va desde 0%el eje vertical del bicono hasta 100%la superficie del bicono. Cuando la saturación está 0%(en el eje vertical del bicono), el tono ya no importa; obtenemos exactamente el mismo gris para todos los tonos en el mismo plano horizontal.

El “mismo plano horizontal” significa tener la misma luminosidad, que aumenta a lo largo del eje vertical del bicono, yendo desde 0%el blackvértice del bicono hasta 100%el whitevértice del bicono. Cuando la luminosidad es 0%o 100%, ya no importan el tono ni la saturación: siempre obtenemos blackun valor de luminosidad de 0%y whiteun valor de luminosidad de 100%.

Dado que solo necesitamos blacky whitepara nuestro símbolo ☯, el tono y la saturación son irrelevantes, por lo que los ponemos a cero y luego cambiamos entre blacky whitecambiando la luminosidad entre 0%y 100%.

, :after { /* other styles that are irrelevant here */ --i: 0; /* lightness of border-color when * --i: 0 is (1 - 0)*100% = 1*100% = 100% (white) * --i: 1 is (1 - 1)*100% = 0*100% = 0% (black) */ border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%)); /* x coordinate of transform-origin when * --i: 0 is 0*100% = 0% (left) * --i: 1 is 1*100% = 100% (right) */ transform-origin: calc(var(--i)*100%) 50%; /* lightness of background-color when * --i: 0 is 0*100% = 0% (black) * --i: 1 is 1*100% = 100% (white) */ background: hsl(0, 0%, calc(var(--i)*100%)); /* animation-delay when * --i: 0 is 0*-$t = 0s * --i: 1 is 1*-$t = -$t */ animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate; } :after { --i: 1 }}

Tenga en cuenta que este enfoque no funciona en Edge debido a que Edge no admite calc()valores para animation-delay.

¿Pero qué pasa si queremos tener un valor distinto de cero cuando el interruptor está apagado ( --i: 0) y otro valor distinto de cero cuando el interruptor está encendido ( --i: 1)?

Cambiar entre dos valores distintos de cero

Digamos que queremos que un elemento tenga un gris background( #ccc) cuando el interruptor esté apagado ( --i: 0) y un naranja background( #f90) cuando el interruptor esté encendido ( --i: 1).

Lo primero que hacemos es cambiar de hexadecimal a un formato más manejable como por ejemplo rgb()o hsl().

Podríamos hacerlo manualmente usando una herramienta como CSS Colors de Lea Verou o a través de DevTools. Si tenemos un backgroundconjunto en un elemento, podemos recorrer los formatos manteniendo Shiftpresionada la tecla mientras hacemos clic en el cuadrado (o círculo) que se encuentra frente al valor en DevTools. Esto funciona tanto en Chrome como en Firefox, aunque no parece funcionar en Edge.

valor.”/

Aún mejor, si usamos Sass, podemos extraer los componentes con las funciones red()// o // .green()blue()hue()saturation()lightness()

Si bien rgb()puede ser el formato más conocido, tiendo a preferirlo hsl()porque lo encuentro más intuitivo y me resulta más fácil tener una idea de qué esperar visualmente con solo mirar el código.

Así, extraemos los tres componentes de los hsl()equivalentes de nuestros dos valores ( $c0: #ccccuando el interruptor está apagado y $c1: #f90cuando el interruptor está encendido) usando estas funciones:

, :after { box-sizing: border-box; margin: 0; padding: 0; font: inherit}html { overflow-x: hidden }body { display: flex; align-items: center; justify-content: center; margin: 0 auto; min-width: 400px; min-height: 100vh; background: #252525}[id='search-btn'] { position: absolute; left: -100vh}

Hasta ahora, todo bien…

¿Así que lo que? Tenemos que admitir que no es nada emocionante, ¡así que pasemos al siguiente paso!

Convertimos la casilla de verificación labelen un gran botón verde redondo y movemos su contenido de texto fuera de la vista usando un gran valor negativo text-indenty overflow: hidden.

*/[for='search-btn'] { overflow: hidden; width: $btn-d; height: $btn-d; border-radius: 50%; box-shadow: 0 0 1.5em rgba(#000, .4); background: #d9eb52; text-indent: -100vw; cursor: pointer;}

A continuación, perfeccionamos la barra de búsqueda actual:

  • dándole dimensiones explícitas
  • proporcionando un backgroundpara su estado normal
  • definiendo un backgroundbrillo diferente y por su estado enfocado
  • redondeando las esquinas del lado izquierdo usando un border-radiusque es igual a la mitad de suheight
  • Limpiando un poco el marcador de posición

*/[id='search-bar'] { border: none; padding: 0 1em; width: $bar-w; height: $bar-h; border-radius: $bar-r 0 0 $bar-r; background: #3f324d; color: #fff; font: 1em century gothic, verdana, arial, sans-serif; ::placeholder { opacity: .5; color: inherit; font-size: .875em; letter-spacing: 1px; text-shadow: 0 0 1px, 0 0 2px } :focus { outline: none; box-shadow: 0 0 1.5em $bar-c, 0 1.25em 1.5em rgba(#000, .2); background: $bar-c; color: #000; }}

En este punto, el borde derecho de la barra de búsqueda coincide con el borde izquierdo del botón. Sin embargo, queremos un poco de superposición; digamos una superposición tal que el borde derecho de la barra de búsqueda coincida con la línea media vertical del botón. Dado que tenemos un diseño de flexbox con align-items: centeren el contenedor ( bodyen nuestro caso), el conjunto formado por nuestros dos elementos (la barra y el botón) permanece alineado horizontalmente en el medio incluso si establecemos un marginen uno o en el otro o en ambos entre esos elementos. (A la izquierda del elemento más a la izquierda o a la derecha del elemento más a la derecha es una historia diferente, pero no entraremos en eso ahora).

Esa es una superposición de .5*$btn-dmenos medio diámetro de botón, lo que equivale al radio del botón. Establecemos esto como negativo margin-righten la barra. También ajustamos el paddinga la derecha de la barra para compensar la superposición:

*/[id='search-bar'] { /* same as before */ margin-right: -$btn-r; padding: 0 calc(#{$btn-r} + 1em) 0 1em;}

Ahora tenemos la barra y el botón en las posiciones para el estado expandido:

Excepto que la barra sigue al botón en el orden DOM, por lo que se coloca encima de él, cuando en realidad queremos que el botón esté encima. Afortunadamente, esto tiene una solución fácil (al menos por ahora; no será suficiente más adelante, pero tratemos un problema a la vez).

*/ position: relative;}

Ahora que le hemos dado al botón un positionvalor no estático, está en la parte superior de la barra:

En este estado, el ancho total del conjunto de barra y botón es el ancho de la barra $bar-wmás el radio del botón $btn-r(que es la mitad del diámetro del botón $btn-d) porque tenemos una superposición para la mitad del botón. En estado colapsado, el ancho total del conjunto es solo el diámetro del botón $btn-d.

Como queremos mantener el mismo eje central al pasar del estado expandido al contraído, necesitamos desplazar el botón hacia la izquierda la mitad del ancho del conjunto en el estado expandido ( .5*($bar-w + $btn-r)) menos el radio del botón ( $btn-r).

A esto lo llamamos desplazamiento $xy lo usamos con el signo menos en el botón (ya que desplazamos el botón hacia la izquierda y la izquierda es la dirección negativa del eje x ). Como queremos que la barra se colapse dentro del botón, le asignamos el mismo desplazamiento $x, pero en la dirección positiva (ya que desplazamos la barra hacia la derecha del eje x ).

Estamos en estado contraído cuando la casilla de verificación no está marcada y en estado expandido cuando no lo está. Esto significa que nuestra barra y botón se desplazan con un CSS transformcuando la casilla de verificación no está marcada y en la posición en la que los tenemos actualmente (no transform) cuando la casilla de verificación está marcada.

Para ello, establecemos una variable --ien los elementos que siguen a nuestra casilla de verificación: el botón (creado con el labelpara la casilla de verificación) y la barra de búsqueda. Esta variable está 0en estado contraído (cuando ambos elementos están desplazados y la casilla de verificación no está marcada) y 1en estado expandido (cuando nuestra barra y botón están en las posiciones que ocupan actualmente, sin desplazamiento, y la casilla de verificación está marcada).

*/ /* if --i is 0, --j is 1 = our translation amount is -$x * if --i is 1, --j is 0 = our translation amount is 0 */ transform: translate(calc(var(--j)*#{-$x}));}[id='search-bar'] { /* same as before */ /* if --i is 0, --j is 1 = our translation amount is $x * if --i is 1, --j is 0 = our translation amount is 0 */ transform: translate(calc(var(--j)*#{$x}));}

¡Y ahora tenemos algo interactivo! Al hacer clic en el botón, se alterna el estado de la casilla de verificación (porque el botón se ha creado utilizando el labelde la casilla de verificación).

Excepto que ahora es un poco difícil hacer clic en el botón, ya que está nuevamente debajo de la entrada de texto (porque hemos establecido un transformen la barra y esto establece un contexto de apilamiento). La solución es bastante sencilla: necesitamos agregar un z-indexal botón y esto lo mueve por encima de la barra.

*/ z-index: 1;}

Pero todavía tenemos otro problema mayor: podemos ver la barra que sale de debajo del botón en el lado derecho. Para solucionar este problema, configuramos clip-pathun inset()valor en la barra. Esto especifica un rectángulo de recorte con la ayuda de las distancias desde los bordes superior, derecho, inferior e izquierdo del elemento border-box. Todo lo que está fuera de este rectángulo de recorte se recorta y solo se muestra lo que está dentro.

En la ilustración de arriba, cada distancia va hacia adentro desde los bordes del cuadro de borde. En este caso, son positivos. Pero también pueden ir hacia afuera, en cuyo caso son negativos y los bordes correspondientes del rectángulo de recorte están fuera del elemento border-box.

Al principio, podríamos pensar que no tenemos motivos para hacerlo, pero en nuestro caso particular, ¡los tenemos!

Queremos que las distancias desde la parte superior ( dt), inferior ( db) e izquierda ( dl) sean negativas y lo suficientemente grandes como para contener el box-shadowelemento que se extiende fuera border-boxdel :focusestado, ya que no queremos que quede recortado. Entonces, la solución es crear un rectángulo de recorte con bordes fuera del elemento border-boxen estas tres direcciones.

La distancia desde la derecha ( dr) es el ancho completo de la barra $bar-wmenos el radio del botón $btn-ren el caso contraído (casilla de verificación no marcada --i: 0) y 0en el caso expandido (casilla de verificación marcada --i: 1).

*/ clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);}

Ahora tenemos una barra de búsqueda y un conjunto de botones que se expande y contrae al hacer clic en el botón.

Como no queremos un cambio abrupto entre los dos estados, utilizamos un transition:

*/ ~ * { /* same as before */ transition: .65s; }}

También queremos que nuestros botones backgroundsean verdes en el caso de que estén contraídos (casilla de verificación no marcada --i: 0) y rosados ​​en el caso de que estén expandidos (casilla de verificación marcada --i: 1). Para esto, usamos la misma técnica que antes:

*/ $c0: #d9eb52; // green for collapsed state $c1: #dd1d6a; // pink for expanded state $h0: round(hue($c0)/1deg); $s0: round(saturation($c0)); $l0: round(lightness($c0)); $h1: round(hue($c1)/1deg); $s1: round(saturation($c1)); $l1: round(lightness($c1)); background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), calc(var(--j)*#{$s0} + var(--i)*#{$s1}), calc(var(--j)*#{$l0} + var(--i)*#{$l1}));}

¡Ahora sí que vamos por buen camino!

Lo que todavía nos queda por hacer es crear el icono que se transforma entre una lupa en el estado contraído y una “x” en el estado expandido para indicar una acción de cierre. Hacemos esto con los pseudoelementos :beforey :after. Comenzamos por decidir un diámetro para la lupa y qué parte de este diámetro representa el ancho de las líneas del icono.

a background, ya que este será el mango de nuestra lupa, hacemos la :afterronda con border-radiusy le damos un recuadro box-shadow.

*/ :before, :after { position: absolute; top: 50%; left: 50%; margin: -.5*$ico-d; width: $ico-d; height: $ico-d; transition: inherit; content: '' } :before { margin-top: -.4*$ico-w; height: $ico-w; background: currentColor } :after { border-radius: 50%; box-shadow: 0 0 0 $ico-w currentColor } }

Ahora podemos ver los componentes de la lupa en el botón:

Para que nuestro icono se parezca más a una lupa, desplazamos translateambos componentes hacia afuera en un cuarto del diámetro de la lupa. Esto significa trasladar el mango hacia la derecha, en la dirección positiva del eje x.25*$ico-d , y la parte principal hacia la izquierda, en la dirección negativa del eje x , en el mismo sentido .25*$ico-d.

También scalemanejamos (el :beforepseudoelemento) horizontalmente hasta la mitad widthcon respecto a su borde derecho (lo que significa a transform-originlo 100%largo del eje x ).

Solo queremos que esto suceda en el estado colapsado (casilla de verificación no marcada, --ies 0y, en consecuencia, --jes 1), por lo que multiplicamos las cantidades de traducción por --jy también usamos --jpara condicionar el factor de escala:

*/ :before { /* same as before */ height: $ico-w; transform: /* collapsed: not checked, --i is 0, --j is 1 * translation amount is 1*.25*$d = .25*$d * expanded: checked, --i is 1, --j is 0 * translation amount is 0*.25*$d = 0 */ translate(calc(var(--j)*#{.25*$ico-d})) /* collapsed: not checked, --i is 0, --j is 1 * scaling factor is 1 - 1*.5 = 1 - .5 = .5 * expanded: checked, --i is 1, --j is 0 * scaling factor is 1 - 0*.5 = 1 - 0 = 1 */ scalex(calc(1 - var(--j)*.5)) } :after { /* same as before */ transform: translate(calc(var(--j)*#{-.25*$ico-d})) } }

Ahora tenemos el ícono de la lupa en estado contraído:

Como queremos que ambos componentes del icono roten 45deg, agregamos esta rotación en el botón mismo:

*/ transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);}

Ahora tenemos el aspecto que queremos para el estado colapsado:

Esto aún deja el estado expandido, donde necesitamos convertir el :afterpseudoelemento redondo en una línea. Lo hacemos al reducir su escala a lo largo del eje x y llevar su border-radiusvalor de 50%a 0%. El factor de escala que usamos es la relación entre el ancho $ico-wde la línea que queremos obtener y el diámetro $ico-ddel círculo que forma en el estado colapsado. Hemos llamado a esta relación $ico-f.

Dado que solo queremos hacer esto en el estado expandido, cuando la casilla de verificación está marcada y --ies 1, hacemos que tanto el factor de escala como el border-radiusdependan de --iy --j:

*

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion