Sombras internas: hundir, delimitar y sustituir al borde
Qué forma tiene realmente una sombra inset, por qué es el mejor anillo de un píxel que ofrece CSS, y los patrones que resuelve sin tocar el modelo de caja.
La palabra inset invierte la geometría de la sombra: en lugar de proyectarse hacia fuera desde el rectángulo, se proyecta hacia dentro desde el hueco. Ese cambio abre tres usos que no tienen nada que ver con simular profundidad, y uno de ellos —el borde que no ocupa espacio— resuelve un problema que arrastra cualquier interfaz con estados.
- Describir la geometría de una sombra interior y cómo la altera el spread.
- Sustituir un borde por una sombra interior sin cambiar el layout.
- Componer varias sombras internas para simular un hundimiento creíble.
- Reconocer los tres sitios donde
insetno funciona.
La geometría invertida
Una sombra exterior toma el rectángulo de la caja de borde, lo desplaza, y pinta lo que queda fuera de la caja original. Una sombra interior toma la caja de relleno, la desplaza, y pinta lo que queda dentro de la caja original y fuera del rectángulo desplazado.
Dicho de otra forma: la sombra exterior es la silueta del objeto proyectada sobre el suelo, y la interior es la sombra que proyectaría el borde del hueco sobre el fondo del hueco.
El spread se comporta al revés en consecuencia. Un spread positivo en una sombra interior la hace crecer hacia dentro, invadiendo más superficie; uno negativo la retrae hacia el borde.
/* Un hundimiento con luz desde arriba. */
.hueco {
box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.3);
}
/* Un vineteado interior en los cuatro lados. */
.vineteado {
box-shadow: inset 0 0 40px rgb(0 0 0 / 0.4);
}
La sombra interior se pinta encima del fondo del elemento y debajo del borde. Eso significa que oscurece el fondo propio, cosa que la exterior no puede hacer, y que un borde declarado sigue viéndose por encima de ella.
El borde que no ocupa espacio
El uso más valioso de inset no tiene nada que ver con la profundidad. Es este:
.campo {
box-shadow: inset 0 0 0 1px #45475a;
}
.campo:focus-visible {
box-shadow: inset 0 0 0 2px #89b4fa;
}
Una sombra interior sin desplazamiento ni desenfoque, con un spread igual al grosor deseado, produce un anillo interior de ese grosor que sigue el border-radius y no ocupa layout.
Compáralo con la alternativa clásica:
/* El problema de siempre. */
.campo { border: 1px solid #45475a; }
.campo:focus-visible { border: 2px solid #89b4fa; }
Al cambiar de uno a dos píxeles, la caja de contenido encoge dos píxeles y el texto de dentro se mueve. Con quince campos en un formulario, cada foco produce un microsalto. Los apaños habituales —compensar con relleno, declarar siempre dos píxeles y cambiar solo el color— funcionan y son ruido. La sombra interior no necesita ninguno: el borde cambia de grosor y nada se mueve, porque la sombra no participa en el layout.
El mismo patrón vale para tablas, listas, tarjetas seleccionables y cualquier componente cuyo contorno cambie con el estado.
Y se puede apilar con la sombra exterior en la misma declaración, que es lo que produce un foco de dos anillos con separación:
.boton:focus-visible {
box-shadow:
inset 0 0 0 1px #1e1e2e,
0 0 0 3px color-mix(in oklab, #89b4fa 60%, transparent);
}
El modo de alto contraste del sistema elimina las sombras y los fondos para imponer su propia paleta. Un contorno construido solo con box-shadow deja de existir ahí. Si el contorno transmite información —foco, selección, error—, acompáñalo de un outline: 2px solid transparent, que en modo normal no se ve y en modo forzado el sistema lo pinta con su color.
Hundimientos creíbles
Para simular una superficie hundida —un campo, un riel, una ranura— una sola sombra interior no basta, por la misma razón por la que no basta una sola sombra exterior: el borde de un hueco real recibe luz por un lado y la bloquea por el otro.
La composición que funciona son dos sombras opuestas y asimétricas:
.riel {
--sombra: rgb(0 0 0 / 0.35);
--luz: rgb(255 255 255 / 0.06);
background: #181825;
border-radius: 999px;
box-shadow:
inset 0 2px 3px var(--sombra),
inset 0 -1px 0 var(--luz);
}
La primera oscurece la parte superior del interior, que es donde el borde del hueco tapa la luz. La segunda deja una línea clarísima en el borde inferior interior, que es donde la luz rebota. Esa segunda línea es la que hace que el efecto funcione, y es la que casi nadie pone.
El principio general —cada sombra necesita su luz opuesta— vale para todo lo que simule un relieve. Un botón que sobresale lleva la luz arriba y la sombra abajo; un hueco lleva la sombra arriba y la luz abajo. Invertir ese par es la forma más barata de mostrar un estado pulsado.
Dónde no funciona inset
No hay text-shadow interior. text-shadow no acepta la palabra clave. El efecto de texto grabado se consigue con dos text-shadow desplazados en sentidos opuestos, uno oscuro y otro claro, sobre un fondo del mismo tono.
No recorta el contenido. La sombra interior se pinta sobre el fondo pero debajo del contenido: el texto y las imágenes de dentro pasan por encima de ella. Para que un contenido quede oscurecido en los bordes hace falta una máscara o un pseudoelemento superpuesto.
No sustituye a un borde en la caja de desbordamiento. Si el elemento tiene overflow: auto, la sombra interior se pinta respecto a la caja de relleno y no se desplaza con el contenido, así que queda fija sobre el marco. Eso suele ser lo que quieres, pero significa que no sirve para marcar dónde acaba el contenido: para eso están las capas de fondo con background-attachment: local.
Las dos ventajas conocidas del anillo interior sobre border son que no ocupa layout y que puede cambiar de grosor sin mover nada. Hay una tercera que es la que de verdad decide en un sistema de diseño: la sombra interior compone correctamente con el fondo y el borde no siempre. Un border semitransparente se pinta sobre el área de borde, que está fuera del área de relleno, con lo que por debajo de él está el fondo del padre, no el del elemento; un borde con alfa 0.2 sobre una tarjeta clara dentro de un contenedor oscuro se ve oscuro, no claro. Es correcto según el modelo de caja y es casi nunca lo que se quiere. Una sombra interior se pinta sobre el fondo propio del elemento, así que un anillo con alfa 0.2 se mezcla con el fondo de la tarjeta y da exactamente el color que esperabas, independientemente de dónde esté la tarjeta. Eso significa que puedes escribir el contorno de un componente una sola vez, con un color relativo, y que funcionará sobre cualquier fondo: box-shadow: inset 0 0 0 1px color-mix(in oklab, CanvasText 15%, transparent) da un contorno del 15% del color del texto sobre el fondo del componente, en tema claro y en oscuro, dentro de un contenedor o fuera. Con border habría que decidir el color absoluto o aceptar que cambia según dónde se coloque. Y hay una consecuencia menor pero práctica: como todo el contorno es una sola sombra, no existen las esquinas mal empalmadas. Un border de cuatro colores distintos empalma en diagonal en las esquinas, y con colores semitransparentes esa diagonal se ve como una línea más oscura porque los dos bordes se solapan. Con una sombra no hay solape porque no hay cuatro trozos: hay un anillo.
- Coge un formulario con bordes que cambian de grosor al enfocar y busca el microsalto.
- Reescríbelo con
inset 0 0 0 Npxy comprueba que desaparece. - Monta un riel hundido con la pareja sombra y luz, y quita la línea de luz para ver la diferencia.
- Pon un borde semitransparente y un anillo interior semitransparente en dos tarjetas iguales sobre fondos distintos y compara los colores resultantes.