wandres.dev
SOMBRAS Y PROFUNDIDAD · box-shadow, drop-shadow, elevación

box-shadow sigue la caja, drop-shadow sigue el alfa

La diferencia estructural entre las dos sombras de CSS, qué pierdes al cambiar de una a otra, y la asimetría del radio de desenfoque entre blur() y drop-shadow().

⏱ 18 min

box-shadow y filter: drop-shadow() producen resultados parecidos en un rectángulo y resultados completamente distintos en cualquier otra cosa. La primera dibuja la sombra de la caja del modelo de caja; la segunda dibuja la sombra de lo que se ve, canal alfa incluido. De ahí salen todas las diferencias, incluidas dos que no son de aspecto sino de comportamiento.

🎯 Al terminar esta lección sabrás
  • Explicar sobre qué geometría opera cada una de las dos sombras.
  • Enumerar lo que drop-shadow() no tiene y box-shadow sí.
  • Anticipar los efectos colaterales de aplicar filter a un elemento.
  • Recordar la asimetría del radio entre blur() y drop-shadow().

Dos geometrías distintas

box-shadow es una propiedad de fondos y bordes. Opera sobre la caja de borde, un rectángulo con esquinas eventualmente redondeadas, y no sabe nada de lo que hay pintado dentro. Le da igual que el fondo sea transparente, que haya un PNG recortado o que el elemento esté cortado por un clip-path: la sombra es siempre la del rectángulo.

filter: drop-shadow() es un filtro. Opera sobre el resultado de rasterizar el elemento y todos sus descendientes, y usa el canal alfa de esa imagen como silueta. Todo lo que sea opaco proyecta sombra; todo lo que sea transparente no.

La diferencia se ve en cuatro casos concretos:

Caso box-shadow drop-shadow()
Un div rectangular con fondo Igual Igual
Un PNG o SVG con transparencia Sombra del rectángulo Sombra de la figura
Un elemento con clip-path Sombra del rectángulo sin recortar Sombra de la forma recortada
Un elemento con fondo transparente y texto Sombra del rectángulo Sombra de las letras

El tercer caso es el que más se usa. Un globo de diálogo con pico, hecho con clip-path, solo puede tener sombra con drop-shadow():

.globo {
  background: #313244;
  clip-path: polygon(0 0, 100% 0, 100% 80%, 24px 80%, 12px 100%, 12px 80%, 0 80%);
  filter: drop-shadow(0 4px 8px rgb(0 0 0 / 0.4));
}

Lo que drop-shadow no tiene

Tres cosas, y las tres importan.

No tiene spread. La sintaxis acepta dos o tres longitudes: desplazamiento horizontal, vertical y desenfoque. No hay cuarta. Todo el control de “que la sombra sea más grande o más pequeña que el objeto” desaparece, y con él la técnica del spread negativo para que la sombra no asome por arriba.

No tiene inset. No existe la sombra interior en forma de filtro.

No admite una lista. filter sí admite varias funciones, y ahí está la trampa: drop-shadow(A) drop-shadow(B) no produce dos sombras del objeto. Produce la sombra A del objeto, y después la sombra B del objeto ya con su sombra A. Es decir, B es la sombra de la sombra. Con desplazamientos pequeños el resultado se parece a dos sombras y con desplazamientos grandes se ve claramente el error.

/* Dos sombras del objeto. */
.a { box-shadow: 0 2px 4px #0006, 0 8px 16px #0004; }

/* Una sombra del objeto y una sombra de esa sombra. */
.b { filter: drop-shadow(0 2px 4px #0006) drop-shadow(0 8px 16px #0004); }

Para un sistema de elevación por capas, esa diferencia descarta drop-shadow() salvo que la forma lo exija.

Los efectos colaterales de filter

Aplicar cualquier filter distinto de none a un elemento tiene tres consecuencias que no tienen nada que ver con la sombra y que sorprenden a mitad de una depuración.

Crea un contexto de apilamiento. El elemento y todo su subárbol pasan a componerse como una unidad, y ningún z-index de dentro puede salir de ella.

Crea un bloque contenedor para descendientes posicionados. Los descendientes con position: fixed dejan de posicionarse respecto al viewport y pasan a hacerlo respecto a este elemento. Los position: absolute también lo toman como referencia. Es el mismo comportamiento que transform, y es el origen del clásico “mi menú fijo se ha quedado dentro de la tarjeta”.

Fuerza la rasterización del subárbol. Para filtrar hay que tener una imagen que filtrar, así que el elemento y sus hijos se pintan a una textura aparte antes de aplicar el filtro. Con un subárbol grande eso es caro, y además rompe algunas optimizaciones de subpíxel del texto.

Ninguna de las tres se puede evitar. Son el precio de usar filtros, y hay que pagarlo a sabiendas.

La asimetría del radio

Un detalle técnico que casi nadie conoce y que produce desajustes al combinar efectos.

filter: blur(<length>) está definido como una gaussiana cuya desviación típica es el valor que escribes. blur(10px) es una gaussiana de sigma 10.

El tercer valor de drop-shadow() es un radio de desenfoque interpretado como en box-shadow, es decir una gaussiana de sigma igual a la mitad. drop-shadow(0 0 10px) es una gaussiana de sigma 5.

O sea: filter: blur(10px) desenfoca el doble que el desenfoque de filter: drop-shadow(0 0 10px), en la misma propiedad y con la misma palabra. No es un fallo de implementación; es la especificación, que heredó dos convenciones distintas de dos sitios distintos.

Consecuencia práctica: si intentas replicar una sombra hecha con box-shadow usando una copia desenfocada del elemento con blur(), tienes que dividir el radio entre dos. Y si vienes de una herramienta de diseño, comprueba cuál de las dos convenciones usa antes de copiar el número.

ℹ️
El color por defecto y la herencia de currentColor

Igual que en box-shadow, si omites el color en drop-shadow() se usa currentColor. En un icono SVG que hereda el color del texto, eso da una sombra del mismo color que el icono, lo cual casi nunca es lo que quieres pero a veces es exactamente lo que buscabas para un halo.

drop-shadow ve el resultado, y eso incluye lo que tú no querías que viera

La regla “sigue el alfa” es correcta y se queda corta en un punto que muerde: drop-shadow() no opera sobre la silueta del elemento, opera sobre el mapa de píxeles del elemento y todo su subárbol ya compuesto. Todo lo que sea visible ahí proyecta sombra, y eso incluye cosas que no forman parte de la forma que tenías en mente. Un elemento con un fondo semitransparente proyecta una sombra atenuada en proporción a ese alfa, porque el filtro toma el canal alfa tal cual: un fondo con alfa 0.5 da una sombra a la mitad de opacidad, algo que box-shadow nunca hace. Un contenedor con hijos que sobresalen por fuera de su caja proyecta la sombra de esos hijos también, aunque visualmente parezcan elementos aparte. Un elemento con texto y fondo transparente proyecta la sombra de cada letra, incluido el antialiasing de los bordes de los glifos, con lo que la sombra queda más blanda de lo esperado. Y el caso más desconcertante: un elemento que ya tiene box-shadow y al que añades drop-shadow() proyecta la sombra de su propia sombra, porque la sombra de caja forma parte del mapa de píxeles que el filtro recibe. De ahí salen dos reglas de higiene. La primera: aplica drop-shadow() al elemento más ajustado posible, normalmente al propio SVG o a la imagen, nunca a un contenedor con relleno, porque el relleno transparente no proyecta sombra pero cualquier cosa dentro de él sí. La segunda: si necesitas la sombra de una forma pero el elemento tiene contenido, separa la forma en un pseudoelemento o en una capa aparte y filtra solo esa. Es más marcado y es la única forma de que la sombra sea de lo que tú decides y no de lo que resulte que hay pintado.

⚔️ Compara las dos sombras
  1. Aplica las dos a un PNG con transparencia y mira la diferencia.
  2. Recorta un elemento con clip-path y comprueba cuál de las dos sigue la forma.
  3. Encadena dos drop-shadow() con desplazamientos grandes y localiza la sombra de la sombra.
  4. Pon un position: fixed dentro de un elemento con filter y observa dónde acaba.
  5. Iguala visualmente un blur(8px) con el desenfoque de un drop-shadow y verifica el factor dos.