Diferentes formas de obtener sombras degradadas CSS | trucos CSS
Esta es una pregunta que escucho muy a menudo: ¿Es posible crear sombras a partir de degradados en lugar de colores sólidos? No hay una propiedad CSS específica que haga esto (confía en mí, busqué) y cada publicación de blog que encuentras al respecto es básicamente un montón de trucos CSS para aproximar un gradiente. De hecho, cubriremos algunos de ellos a medida que avanzamos.
Pero primero… otro artículo sobre sombras degradadas? ¿En realidad?
Sí, esta es otra publicación sobre el tema, pero es diferente. Juntos superaremos los límites para obtener una solución que cubra algo que no he visto en ningún otro lugar: transparenciaLa mayoría de los trucos funcionan si el elemento tiene un fondo opaco, pero ¿y si tenemos un fondo transparente? ¡Investigaremos este caso aquí!
Antes de comenzar, déjame presentarte mi generador de sombras degradadas。 Todo lo que necesita hacer es ajustar la configuración y obtener el código. Pero sígueme porque te ayudaré a entender toda la lógica detrás del código generado.
Solución opaca
Comencemos con la solución que funcionará el 80 % de las veces. El caso más típico: utiliza un elemento con un fondo y necesita agregarle una sombra paralela degradada. No hay problemas de transparencia a considerar.
La solución es confiar en un pseudo-elemento donde se define el gradiente, lo colocas detrás del elemento real y le aplicas un filtro de desenfoque.
.box {
position: relative;
}
.box::before {
content: "";
position: absolute;
inset: -5px; /* control the spread */
transform: translate(10px, 8px); /* control the offsets */
z-index: -1; /* place the element behind */
background: /* your gradient here */;
filter: blur(10px); /* control the blur */
}
Parece mucho código, y eso es porque lo es. Así es como podríamos hacerlo con box-shadow
En cambio, si usamos un color sólido en lugar de un degradado.
box-shadow: 10px 8px 10px 5px orange;
Esto debería darle una buena idea de lo que hacen los valores en el primer fragmento. Tenemos compensaciones X e Y, el radio de desenfoque y la distancia de propagación. Tenga en cuenta que necesitamos un valor negativo para la distancia de propagación que proviene de inset
Propiedad.
Aquí hay una demostración que muestra la sombra degradada junto a la clásica. box-shadow
:
Si observa de cerca, notará que ambas sombras son ligeramente diferentes, especialmente la parte difuminada. Eso no es una sorpresa, porque estoy bastante seguro de que lo es. filter
algoritmo de propiedad funciona de manera diferente que el for box-shadow
No es gran cosa, ya que el resultado final es bastante similar.
Esta solución es buena, pero todavía hay varias desventajas asociadas con z-index: -1
declaración ¡Sí, ahí es donde ocurre el "contexto de apilamiento"!
apliqué un transform
al elemento principal y boom! La sombra ya no está debajo del elemento. Esto no es un error, sino un resultado lógico de un contexto de pedido. No se preocupe, no comenzaré una explicación aburrida sobre el contexto de apilamiento (Ya he hecho esto en un hilo de desbordamiento de pila), pero le mostraré cómo solucionarlo de todos modos.
La primera solución que recomiendo es usar 3D transform
:
.box {
position: relative;
transform-style: preserve-3d;
}
.box::before {
content: "";
position: absolute;
inset: -5px;
transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
background: /* .. */;
filter: blur(10px);
}
En lugar de usar z-index: -1
vamos a usar una traslación negativa a lo largo del eje Z. Vamos a poner todo dentro translate3d()
No olvides usar transform-style: preserve-3d
en el elemento principal; de lo contrario 3D transform
no surtirá efecto.
Hasta donde yo sé, esta solución no tiene efectos secundarios… pero es posible que vea uno. Si es así, compártelo en la sección de comentarios e intentemos encontrar una solución.
Si por alguna razón no puede usar 3D transform
la otra solución es confiar en dos pseudo-elementos - ::before
y ::after
Uno crea la sombra degradada y el otro renderiza el fondo base (y otros estilos que puedas necesitar) De esta manera podemos controlar fácilmente el orden de los dos pseudo-elementos.
.box {
position: relative;
z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
content: "";
position: absolute;
z-index: -2;
inset: -5px;
transform: translate(10px, 8px);
background: /* .. */;
filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
content: """;
position: absolute;
z-index: -1;
inset: 0;
/* Inherit all the decorations defined on the main element */
background: inherit;
border: inherit;
box-shadow: inherit;
}
Es importante señalar que estamos forzando el elemento principal para crear un contexto de pedido por declaración z-index: 0
o cualquier otra propiedad que haga lo mismoen él. Además, recuerde que los pseudoelementos consideran el campo de relleno del elemento principal como referencia. Entonces, si el elemento principal tiene un borde, debe tenerlo en cuenta cuando defina los estilos del pseudo elemento. Notará que uso inset: -2px
En ::after
para tener en cuenta el borde definido en el elemento padre.
Como dije, esta solución probablemente sea lo suficientemente buena en la mayoría de los casos en los que desea una sombra degradada, siempre que no necesite mantener la transparencia. Pero estamos aquí para el desafío y para ampliar los límites, por lo que ni siquiera necesita lo que sigue, quédese conmigo Probablemente aprenderá nuevos trucos de CSS que puede usar en otros lugares.
Solución clara
Retomemos donde lo dejamos con 3D transform
y elimino el fondo del elemento principal. Comenzaré con una sombra que tenga compensaciones y una distancia de propagación igual a 0
.
La idea es encontrar una manera de recortar u ocultar todo lo que está dentro del área del elemento (dentro del borde verde) y preservar lo que está afuera. Usaremos clip-path
Pero te estarás preguntando cómo clip-path
puede hacer un corte adentro elemento.
De hecho, no hay forma de hacer esto, pero podemos simularlo usando un modelo de polígono concreto:
clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)
¡Tada! Tenemos una sombra degradada que mantiene la transparencia. Todo lo que hicimos fue agregar un clip-path
al código anterior Aquí hay una figura que ilustra la parte del polígono.
El área azul es la parte visible después de aplicar clip-path
. Solo estoy usando el color azul para ilustrar el concepto, pero en realidad solo veremos la sombra dentro de esa área. Como puede ver, tenemos cuatro puntos definidos con un valor grande (B
). Mi gran valor es 100vmax
pero puede ser cualquier valor grande que desee. La idea es asegurarnos de que tenemos suficiente espacio para la sombra. También tenemos cuatro puntos que son las esquinas del pseudo-elemento.
Las flechas ilustran el camino que define el polígono. empezamos desde (-B, -B)
hasta que lleguemos (0,0)
. Necesitamos 10 puntos en total. No ocho puntos porque dos puntos se repiten dos veces en el camino ((-B,-B)
y (0,0)
).
todavía tiene Una cosa más Queda por hacer, y eso es tener en cuenta la distancia de propagación y las compensaciones. La única razón por la que la demostración anterior funciona es porque es un caso especial en el que las compensaciones y la distancia de propagación son iguales a 0
.
Definamos el spread y veamos qué sucede. Recuerda que usamos inset
con un valor negativo para hacer esto:
El pseudo-elemento ya es más grande que el elemento principal, por lo que clip-path
recorta más de lo que necesitamos. Recuerda que siempre tenemos que cortar la pieza adentro el elemento principal (el área dentro del borde verde del ejemplo). Necesitamos ajustar la posición de los cuatro puntos dentro del clip-path
.
.box {
--s: 10px; /* the spread */
position: relative;
}
.box::before {
inset: calc(-1 * var(--s));
clip-path: polygon(
-100vmax -100vmax,
100vmax -100vmax,
100vmax 100vmax,
-100vmax 100vmax,
-100vmax -100vmax,
calc(0px + var(--s)) calc(0px + var(--s)),
calc(0px + var(--s)) calc(100% - var(--s)),
calc(100% - var(--s)) calc(100% - var(--s)),
calc(100% - var(--s)) calc(0px + var(--s)),
calc(0px + var(--s)) calc(0px + var(--s))
);
}
Hemos definido una variable CSS, --s
para la distancia de propagación y actualiza los puntos del polígono. No toqué los puntos donde uso el valor grande. Solo actualizo los puntos que definen las esquinas del pseudo-elemento. Incremento todos los valores nulos por --s
y reducir 100%
valores de --s
.
La lógica es la misma con las compensaciones. Cuando trasladamos el pseudo-elemento, la sombra no está alineada y necesitamos ajustar el polígono nuevamente y mover los puntos en la dirección opuesta.
.box {
--s: 10px; /* the spread */
--x: 10px; /* X offset */
--y: 8px; /* Y offset */
position: relative;
}
.box::before {
inset: calc(-1 * var(--s));
transform: translate3d(var(--x), var(--y), -1px);
clip-path: polygon(
-100vmax -100vmax,
100vmax -100vmax,
100vmax 100vmax,
-100vmax 100vmax,
-100vmax -100vmax,
calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
calc(0px + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
calc(100% - var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y))
);
}
Hay dos variables más para la compensación: --x
y --y
Los usamos adentro transform
y también actualizamos clip-path
Todavía no tocamos los puntos del polígono con valores grandes, pero compensamos todos los demás: reducimos --x
de las coordenadas X y --y
de las coordenadas Y.
Ahora todo lo que tenemos que hacer es actualizar algunas variables para controlar la sombra del degradado. Y mientras estamos en eso, también hagamos que el radio de desenfoque sea variable:
¿Todavía necesitamos 3D?
transform
¿truco?
Todo depende de la frontera. Recuerde que la referencia del pseudoelemento es el campo de relleno, por lo que si aplica un borde a su elemento principal, tendrá una superposición. O guardar 3D transform
truco o actualización de inset
valor para informar de la frontera.
Aquí está la demostración anterior con una versión actualizada inset
valor posicional de 3D transform
:
Diría que esta es una forma más apropiada porque la distancia de propagación será más precisa ya que comienza desde el límite en lugar del campo de pad. Pero tendrás que ajustar inset
valor según el borde del elemento principal. A veces, el límite del elemento es desconocido y debe usar la solución anterior.
Con la solución opaca anterior, es posible que tenga un problema con el contexto de apilamiento. Y con la solución transparente, es posible que enfrente un problema de límites. Ahora tiene opciones y formas de solucionar estos problemas. El truco de transformación 3D es mi solución favorita porque resuelve todos los problemas (El generador en línea también lo investigaré)
Agregar un radio al borde
Si intenta agregar border-radius
al elemento cuando usamos la solución opaca con la que comenzamos, esta es una tarea bastante trivial. Todo lo que tiene que hacer es heredar el mismo valor del elemento principal y listo.
Incluso si no tiene un radio de borde, es una buena idea definirlo border-radius: inherit
Esto da cuenta de cualquier posible border-radius
Es posible que desee agregar más tarde o un radio de borde que provenga de otro lugar.
Es una historia diferente cuando se trata de la solución transparente. Desafortunadamente, esto significa encontrar otra solución porque clip-path
no puede manejar curvas Esto significa que no podremos cortar el área dentro del elemento principal.
te presentaremos mask
propiedad de la mezcla.
Esta parte fue muy tediosa y luché por encontrar una solución general que no dependiera de números mágicos. Terminé con una solución muy compleja que solo usaba un pseudoelemento, pero el código era un trozo de espagueti que solo cubría algunos casos específicos. no creo que valga la pena explorando este camino.
Decidí insertar un elemento adicional en el nombre del código más simple. Aquí está la marca:
<div class="box">
<sh></sh>
</div>
Yo uso un elemento personalizado, <sh>
para evitar posibles conflictos con CSS externo. Podría haber usado un <div>
pero dado que es un elemento genérico, puede ser objeto fácilmente de otra regla CSS que provenga de otro lugar, lo que puede romper nuestro código.
El primer paso es posicionar <sh>
e intencionalmente crea un desbordamiento:
.box {
--r: 50px;
position: relative;
border-radius: var(--r);
}
.box sh {
position: absolute;
inset: -150px;
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
}
El código puede parecer un poco extraño, pero veremos la lógica detrás de él a medida que avanzamos. A continuación, creamos la sombra degradada usando un pseudo elemento de <sh>
.
.box {
--r: 50px;
position: relative;
border-radius: var(--r);
transform-style: preserve-3d;
}
.box sh {
position: absolute;
inset: -150px;
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
transform: translateZ(-1px)
}
.box sh::before {
content: "";
position: absolute;
inset: -5px;
border-radius: var(--r);
background: /* Your gradient */;
filter: blur(10px);
transform: translate(10px,8px);
}
Como puede ver, el pseudoelemento usa el mismo código que todos los ejemplos anteriores. La única diferencia es 3D. transform
determinado en <sh>
en lugar del pseudo elemento. Por ahora, tenemos una sombra degradada sin la función de transparencia:
Tenga en cuenta que el área de <sh>
elemento está definido por el contorno negro. ¿Por qué estoy haciendo esto? Porque de esta manera puedo aplicar un mask
en él para ocultar la parte dentro del área verde y mantener la parte de desbordamiento donde necesitamos ver la sombra.
Sé que es un poco difícil, pero a diferencia de clip-path
en mask
la propiedad no tiene en cuenta el área afuera elemento para mostrar y ocultar cosas Es por eso que tuve que introducir el elemento adicional: para simular el área "exterior".
También tenga en cuenta que uso una combinación de border
y inset
Esto me permite mantener el campo de relleno de este elemento adicional igual que el elemento principal para que el pseudo elemento no necesite cálculos adicionales.
Otra cosa útil que obtenemos al usar un elemento adicional es que el elemento está fijo y solo se mueve el pseudoelemento (usando translate
). Esto me permitirá definir fácilmente la máscara que es último paso de este truco.
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
¡Listo! Tenemos nuestra sombra degradada y es compatible border-radius
Probablemente esperabas un complejo mask
valor con múltiples gradientes, ¡pero no! Solo necesitamos dos degradados simples y un mask-composite
para completar el hechizo.
vamos a aislar <sh>
elemento para averiguar qué está pasando allí:
.box sh {
position: absolute;
inset: -150px;
border: 150px solid red;
background: lightblue;
border-radius: calc(150px + var(--r));
}
Esto es lo que obtenemos:
Observe cómo el radio interior coincide con el del elemento principal border-radius
Establecí un límite grande (150px
) y un border-radius
igual al límite grande más el radio del elemento padre. Por fuera, tengo un radio igual a 150px + R
dentro tengo 150px + R - 150px = R
.
Necesitamos ocultar la parte interna (azul) y asegurarnos de que la parte del borde (roja) aún esté visible. Para hacer esto, he definido dos máscaras de capa: una que cubre solo el área del cuadro de contenido y otra que cubre el área del borde del cuadro (el valor predeterminado). Luego desconecté uno del otro para revelar el borde.
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
Usé la misma técnica para crear un marco que admita degradados y border-radius
Ana Tudor también tiene un buen artículo sobre composite de enmascaramiento que te invito a leer.
¿Hay alguna desventaja en este método?
Sí, esto definitivamente no es perfecto. El primer problema con el que te puedes encontrar está relacionado con el uso de un borde en el elemento principal. Esto puede crear una pequeña discrepancia en los radios si no lo tiene en cuenta. Tenemos este problema en nuestro ejemplo, pero es posible que apenas lo notes.
La solución es relativamente fácil: agregue el ancho del marco para <sh>
elementos inset
.
.box {
--r: 50px;
border-radius: var(--r);
border: 2px solid;
}
.box sh {
position: absolute;
inset: -152px; /* 150px + 2px */
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
}
Otro inconveniente es el gran valor que usamos para el límite (150px
en el ejemplo). Este valor debe ser lo suficientemente grande para contener la sombra, pero no demasiado grande para evitar problemas de desbordamiento y barra de desplazamiento. Afortunadamente, el generador en línea calculará el valor óptimo considerando todos los parámetros.
El último inconveniente que conozco es cuando trabajas con un complejo border-radius
Por ejemplo, si desea aplicar un radio diferente a cada esquina, debe definir una variable para cada lado. Supongo que no es una desventaja, pero puede hacer que su código sea un poco más difícil de mantener.
.box {
--r-top: 10px;
--r-right: 40px;
--r-bottom: 30px;
--r-left: 20px;
border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
El generador en línea solo mira un radio uniforme por simplicidad, pero ya sabe cómo modificar el código si quiere ver una configuración de radio compleja.
resumiendo
¡Hemos llegado al final! La magia detrás de las sombras degradadas ya no es un misterio. He tratado de cubrir todas las posibilidades y posibles problemas a los que te puedes enfrentar. Si me he perdido algo o si encuentra algún problema, no dude en informarlo en la sección de comentarios y lo revisaré.
Una vez más, es probable que gran parte de esto sea excesivo dado que la solución de facto cubrirá la mayoría de sus casos de uso. Sin embargo, es bueno saber el "por qué" y el "cómo" detrás del truco y cómo superar sus limitaciones. Además, tenemos un buen ejercicio jugando con el recorte y el enmascaramiento de CSS.
Y por supuesto que tienes el generador en línea puede comunicarse en cualquier momento para no meterse en problemas.
Deja una respuesta