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

Un sistema de elevación con sombras superpuestas

Por qué una sola sombra siempre parece falsa, cómo se aproxima la caída real de una penumbra con capas en progresión geométrica, y qué color debe tener una sombra que no sea negro.

⏱ 22 min

Todo el mundo ha escrito box-shadow: 0 4px 12px rgba(0,0,0,0.15) y ha pensado que quedaba bien, y todo el mundo ha visto una interfaz donde las sombras se ven de verdad y no ha sabido decir qué hacía distinto. La respuesta está en dos decisiones, las dos contraintuitivas: no usar una sombra sino cinco, y no usar el color negro. Una vez las ves, no se pueden dejar de ver.

🎯 Al terminar esta lección sabrás
  • Explicar por qué el perfil de una sola gaussiana se lee como artificial.
  • Construir una escala de elevación con capas en progresión geométrica.
  • Elegir el color de la sombra en función de la superficie, no del negro.
  • Adaptar el sistema al tema oscuro, donde las sombras dejan de funcionar.

Por qué una sola sombra parece falsa

Una sombra real no la produce un punto de luz: la producen fuentes con superficie —una ventana, un panel, el cielo—, y eso genera una penumbra, la zona de transición donde la fuente está parcialmente tapada. La anchura de esa penumbra crece con la distancia entre el objeto y la superficie. Justo en el punto de contacto la sombra es casi perfectamente nítida y muy oscura; a diez centímetros ya es difusa; a un metro apenas se distingue.

Un box-shadow con desenfoque es una gaussiana de una sola escala. Su perfil de opacidad, medido perpendicularmente al borde, es siempre el mismo: sube desde cero hasta el máximo en una distancia fija, con un punto de inflexión bien definido en el medio. Eso no se parece a nada que ocurra en el mundo.

Y hay una razón perceptual por la que el desajuste se nota tanto. El sistema visual humano no mide luminancia absoluta: mide contraste local, mediante inhibición lateral entre células vecinas de la retina. Eso lo hace extraordinariamente sensible a los cambios en la pendiente de la luminancia, no en su valor. El punto de inflexión de una gaussiana es exactamente un cambio brusco de pendiente, y el ojo lo detecta como un borde aunque la transición sea suave. Es el mismo mecanismo que produce las bandas de Mach, esas franjas ilusorias que aparecen en la unión entre dos degradados. Tu sombra de una capa tiene un borde fantasma, y por eso se lee como una mancha pegada debajo del elemento en vez de como ausencia de luz.

La forma de arreglarlo es superponer varias gaussianas de escalas distintas. La suma de gaussianas con desviación típica en progresión geométrica y amplitud decreciente aproxima una caída sin inflexión marcada, mucho más parecida a la de una penumbra real, y sin ningún punto donde la pendiente cambie de golpe.

La escala en progresión geométrica

La receta es doblar el desplazamiento y el desenfoque en cada capa, y mantener el spread negativo en la mitad del desplazamiento:

:root {
  --tono-sombra: 264;
  --s1: oklch(25% 0.04 var(--tono-sombra) / 0.10);
  --s2: oklch(25% 0.04 var(--tono-sombra) / 0.08);
  --s3: oklch(25% 0.04 var(--tono-sombra) / 0.06);

  --elev-1:
    0 1px 1px  -0.5px var(--s1),
    0 2px 2px  -1px   var(--s2);

  --elev-2:
    0 1px 1px  -0.5px var(--s1),
    0 2px 2px  -1px   var(--s2),
    0 4px 4px  -2px   var(--s2),
    0 8px 8px  -4px   var(--s3);

  --elev-3:
    0 1px 1px   -0.5px var(--s1),
    0 2px 2px   -1px   var(--s2),
    0 4px 4px   -2px   var(--s2),
    0 8px 8px   -4px   var(--s3),
    0 16px 16px -8px   var(--s3),
    0 32px 32px -16px  var(--s3);
}

.tarjeta { box-shadow: var(--elev-1); }
.menu    { box-shadow: var(--elev-2); }
.dialogo { box-shadow: var(--elev-3); }

Cuatro observaciones sobre esa tabla, porque cada número está donde está por una razón.

La primera capa es la sombra de contacto. Un píxel de desplazamiento, un píxel de desenfoque, medio píxel de spread negativo. Es casi una línea, es la capa más opaca, y es la que hace que el objeto se lea como apoyado en algo en lugar de flotando. Quítala y el elemento entero parece despegarse.

Cada capa dobla la anterior. 1, 2, 4, 8, 16, 32. Esa progresión geométrica es la que hace que las escalas se solapen sin dejar huecos y sin acumular todas su inflexión en el mismo sitio.

La opacidad baja, pero poco. De 0.10 a 0.06. La tentación es hacerla caer mucho más rápido, y el resultado es que las capas grandes desaparecen y vuelves a tener una sola sombra. Lo que produce la caída total es la acumulación de capas, no la caída de cada una.

Subir de elevación es añadir capas, no agrandar la sombra. --elev-2 contiene enteras las capas de --elev-1. Eso es lo que hace que la transición entre niveles se lea como que el objeto sube y no como que la sombra cambia, y es también lo que permite interpolar entre dos niveles sin saltos raros.

El color de la sombra

La segunda decisión. Una sombra no es una capa negra semitransparente: es la superficie de debajo iluminada solo por luz ambiente, sin la luz directa.

Poner negro con alfa equivale a multiplicar la superficie por un gris, y multiplicar por un gris desatura. Una superficie de color con una sombra negra encima pierde saturación en la zona sombreada, y el resultado se lee como suciedad. Es el mismo problema que interpolar colores en sRGB, visto desde otro sitio.

Lo que hay que hacer es teñir la sombra:

:root {
  /* Mal: gris muerto sobre cualquier superficie. */
  --sombra-mala: rgb(0 0 0 / 0.15);

  /* Bien: oscura, con algo de croma, del lado frio. */
  --sombra-buena: oklch(25% 0.04 264 / 0.12);
}

El criterio para elegir el tono tiene fundamento físico: en un entorno normal la luz directa viene de una fuente cálida y la luz ambiente que llena las sombras viene del cielo o del rebote de las paredes, que es más fría. Por eso las sombras de las fotografías tienden al azul. Un tono entre 240 y 280 grados con croma bajo, entre 0.02 y 0.06, funciona en casi cualquier interfaz.

Si la superficie tiene color propio, hay una versión mejor todavía: derivar la sombra de la propia superficie.

.tarjeta {
  --superficie: oklch(97% 0.01 264);
  background: var(--superficie);
  box-shadow: 0 8px 24px -8px oklch(from var(--superficie) 0.28 calc(c * 4) h / 0.18);
}

La sombra conserva el tono de la superficie, con la claridad baja y el croma multiplicado. Eso es lo que hace de verdad una sombra sobre una superficie de color, y ninguna cantidad de negro lo reproduce.

El tema oscuro

Aquí está el problema que rompe la mitad de los sistemas de elevación: sobre un fondo casi negro, una sombra oscura no se ve. El contraste entre #11111b y #0d0d14 es prácticamente nulo, y subir la opacidad solo produce un cerco sucio.

La respuesta correcta no es forzar la sombra: es cambiar de señal. En un entorno oscuro, lo que indica que algo está más cerca de ti no es que proyecte más sombra, es que recibe más luz. Así que la elevación se codifica aclarando la superficie.

:root {
  --superficie-0: oklch(16% 0.015 264);
  --superficie-1: oklch(20% 0.015 264);
  --superficie-2: oklch(24% 0.015 264);
  --superficie-3: oklch(28% 0.015 264);
}

@media (prefers-color-scheme: dark) {
  .tarjeta { background: var(--superficie-1); box-shadow: var(--elev-1); }
  .menu    { background: var(--superficie-2); box-shadow: var(--elev-2); }
  .dialogo { background: var(--superficie-3); box-shadow: var(--elev-3); }
}

Las sombras se quedan porque siguen aportando la separación del borde, pero el trabajo pesado lo hace la escala de superficies. En tema claro es al revés: la superficie es siempre la misma y la sombra hace todo el trabajo.

⚠️
No animes box-shadow entre dos niveles

Cambiar de --elev-1 a --elev-2 en un :hover con transition: box-shadow obliga a repintar el elemento y su sombra en cada fotograma, y con seis capas de desenfoque eso es caro. El patrón barato es tener las dos sombras en dos pseudoelementos superpuestos y cruzar sus opacity, que el compositor resuelve sin repintar nada.

La sombra codifica una jerarquía, y por eso el número de niveles importa más que su aspecto

Cuando un sistema de elevación falla no suele ser porque las sombras estén mal dibujadas: es porque hay demasiados niveles y ninguno significa nada. Una sombra no es decoración, es un canal de información: dice a qué distancia del plano de la página está un elemento, y de ahí el lector deduce qué tapa a qué, qué es modal, qué se puede cerrar y qué es el fondo. Ese canal tiene una capacidad muy limitada, porque la discriminación de profundidad a partir de sombras es aproximadamente logarítmica: para que dos niveles se distingan de forma fiable, la separación entre ellos tiene que ser un factor multiplicativo, no una diferencia. Por eso la progresión geométrica funciona y una escala lineal de cinco pasos de cuatro píxeles cada uno es indistinguible. Y por eso tres niveles es casi siempre el número correcto: apoyado, levantado, flotante. Cuatro se pueden defender si el cuarto es el diálogo modal. Seis niveles significa que dos pares de ellos son visualmente idénticos y que la gente que use el sistema los elegirá al azar, con lo que la señal se destruye. La consecuencia de diseño que se deriva es incómoda para quien viene de una biblioteca con diez sombras predefinidas: si necesitas más de cuatro niveles, el problema no es la escala de sombras, es la arquitectura de la interfaz. Cinco planos de profundidad simultáneos en pantalla no se pueden leer, con sombras ni sin ellas. Y hay un corolario que se pasa por alto en las revisiones: como el canal es la profundidad, dos elementos hermanos que están al mismo nivel de la jerarquía tienen que llevar exactamente la misma sombra, aunque uno sea más importante que el otro. La importancia se codifica con color, tamaño o posición; usar la elevación para eso rompe la única cosa que la elevación sabe decir.

⚔️ Construye tu escala
  1. Pinta un elemento con 0 4px 12px rgb(0 0 0 / 0.15) y otro con --elev-2, y míralos juntos.
  2. Quita la capa de contacto de la escala y observa cómo el elemento empieza a flotar.
  3. Cambia el color de la sombra de negro a un azul oscuro de croma bajo sobre una superficie de color.
  4. Reproduce la escala en tema oscuro con superficies aclaradas y comprueba cuál de las dos señales hace el trabajo.
  5. Reduce tu sistema actual a tres niveles y comprueba si algo deja de entenderse.