box-shadow : les ombres portées en CSS, du simple au réaliste
Une ombre portée tient en une ligne de CSS, et c'est justement ce qui la rend trompeuse. On recopie une valeur trouvée dans un générateur sans savoir ce que fait chaque nombre, on cherche « border-shadow » parce que l'ombre ressemble à une bordure, puis on découvre un jour qu'une ombre disparaît dans une liste ou qu'elle ignore la forme d'une icône. Ce guide reprend box-shadow valeur par valeur, mesure ce que le navigateur dessine réellement, puis assemble une ombre réaliste.
Méthode : chaque chiffre de cette page sort d'un banc de mesure exécuté le 5 octobre 2026 dans Chromium 153.0.8010.12 (le navigateur de Playwright 1.63.0) piloté depuis Node 20.19.4 sous Windows 11. Les styles sont relus avec getComputedStyle, les dimensions avec offsetWidth, et les ombres elles-mêmes sont lues pixel par pixel sur des captures. Rejoué dans l'image Linux officielle de Playwright 1.63.0 (Node 24.20.0, même Chromium), le banc a produit des captures identiques pixel pour pixel. Si vous découvrez les ombres, commencez par l'introduction aux bordures et aux ombres en CSS : ici, on va plus loin.
Lire une ombre comme le navigateur la lit
Les longueurs viennent dans cet ordre : deux décalages obligatoires (horizontal puis vertical), puis, facultativement, un rayon de flou et une distance d'étalement ; la couleur et le mot-clé inset peuvent se placer avant ou après elles. Le flou ne peut pas être négatif ; l'étalement, si. Le schéma ci-dessous décompose une valeur complète.
Pour voir comment le navigateur range ces valeurs, le banc applique trois règles volontairement désordonnées :
.a {
box-shadow: 10px 10px 5px red;
}
.b {
color: rgb(0, 0, 255);
box-shadow: 4px 4px;
}
.c {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
}Et voici ce que renvoie getComputedStyle(element).boxShadow pour chacune :
.a:rgb(255, 0, 0) 10px 10px 5px 0px;.b:rgb(0, 0, 255) 4px 4px 0px 0px;.c:rgba(0, 0, 0, 0.5) 0px 2px 4px 0px inset.
Quatre enseignements. La couleur passe en tête et le mot-clé red devient rgb(255, 0, 0). Les valeurs omises ne disparaissent pas : elles valent 0px, et le navigateur les écrit. Sans couleur, l'ombre de .b prend la couleur du texte de l'élément, ici le bleu : la spécification indique qu'une couleur absente vaut currentColor. Enfin, le mot-clé inset, écrit en premier dans la règle, se retrouve en dernier. Quand une ombre ne ressemble pas à ce que vous pensiez avoir écrit, c'est cette forme calculée, visible dans l'onglet des styles calculés de l'inspecteur, qu'il faut relire.
Une ombre ne prend aucune place, et c'est un piège autant qu'un atout
La différence fondamentale entre une bordure et une ombre tient dans cette expérience. Quatre boîtes de 200 pixels de large : une sans rien, une avec une ombre pleine de 10 pixels, une avec une bordure de 10 pixels, et une avec la même bordure en box-sizing: border-box.
.boite {
width: 200px;
height: 100px;
}
.avec-ombre {
box-shadow: 0 0 0 10px #4f46e5;
}
.avec-bordure {
border: 10px solid #4f46e5;
}
.avec-bordure-incluse {
border: 10px solid #4f46e5;
box-sizing: border-box;
}À l'écran, l'ombre et la bordure dessinent le même anneau : sur la capture, les 10 premiers pixels sont de la couleur #4f46e5 et le 11e est blanc, dans les deux cas. Mais la boîte ombrée mesure toujours 200 pixels (offsetWidth et getBoundingClientRect().width), alors que la boîte bordée passe à 220 pixels et pousse ce qui l'entoure. Avec border-box, la largeur totale revient à 200 pixels, mais la bordure mange l'intérieur : clientWidth tombe à 180 pixels. Ce réglage est détaillé dans le cours sur les dimensions et la position en CSS. La spécification le dit sans détour : les ombres n'influencent pas la mise en page.
L'atout est évident : on peut ajouter ou grossir une ombre sans que rien ne bouge autour. Le piège l'est moins. Puisqu'une ombre n'occupe aucune place, elle ne crée pas non plus de défilement, et un parent en overflow: auto ou hidden la coupe :
.cadre {
width: 300px;
overflow: auto;
}
.cadre .boite {
width: 300px;
height: 100px;
box-shadow: 40px 0 0 #000;
}L'ombre dépasse de 40 pixels à droite, et pourtant le cadre mesure scrollWidth = 300 pour clientWidth = 300 : aucune barre de défilement. Remplacez l'ombre par une border-right de 40 pixels et scrollWidth passe à 340. Côté pixels, au 21e pixel à droite du cadre, là où l'ombre devrait être, la capture est blanche : overflow: auto l'a coupée. Le module CSS Overflow range les ombres dans le « débordement d'encre » (ink overflow), celui qui se peint sans jamais agrandir la zone défilable.
Un cas typique : une carte dans une liste en overflow: hidden, posée pour masquer un débordement sans rapport :
.liste {
overflow: hidden;
}
.carte {
height: 100px;
box-shadow: 0 8px 0 #000;
}Mesuré au 4e pixel sous la carte : blanc. L'ombre existe, mais elle tombe hors de la liste, qui la coupe. La correction consiste à rendre de la place à l'ombre à l'intérieur du parent :
.liste {
overflow: hidden;
padding: 16px;
}Même mesure, au 4e pixel sous la carte : noir. Pour dimensionner ce padding, additionnez le décalage, l'étalement et la portée du flou, que la section suivante mesure.
Ce que dessine vraiment un flou de 20px
Le troisième nombre mérite une mesure à part. Pour l'observer, le banc dessine une boîte de 100 pixels sans fond, avec une ombre noire centrée sur fond blanc, puis lit une ligne de pixels qui part du bord droit de la boîte :
.flou {
width: 100px;
height: 100px;
box-shadow: 0 0 20px #000;
}Première surprise : l'intérieur de la boîte est blanc, jusqu'au dernier pixel avant le bord. Une ombre extérieure est découpée à l'intérieur de la boîte, comme si celle-ci était opaque. Avec un fond transparent, on voit donc un « trou » à la place de l'élément. À l'extérieur, voici la noirceur relevée (0 % = blanc, 100 % = noir). La distance est comptée du bord jusqu'au pixel mesuré : « 10 px » désigne le 11e pixel extérieur.
| Distance au bord | Noirceur mesurée |
|---|---|
| 1er pixel extérieur | 48,2 % |
| 5 px | 30,2 % |
| 10 px | 15,7 % |
| 15 px | 6,7 % |
| 20 px | 2,0 % |
| 29 px (dernier pixel non blanc) | 0,4 % |
Ce profil s'explique par la spécification : le flou doit approcher, à 5 % près par pixel, un flou gaussien dont l'écart-type vaut la moitié du rayon de flou, soit 10 pixels ici. Le banc compare chaque pixel, de 0 à 30 pixels du bord, à cette courbe théorique : l'écart maximal est de 1,3 point. Une note de la même spécification en tire la règle pratique : sur un bord droit, la transition s'étale sur environ deux fois le rayon, centrée sur le bord. C'est ce que montre le tableau : presque la moitié de l'intensité au bord, 2 % à 20 pixels, puis une traîne jusqu'à 29 pixels qui ne s'écarte plus du blanc que d'un niveau sur 255.
Retenez qu'un flou de N pixels porte l'ombre à environ N pixels au-delà de son bord : c'est ce que prévoit la note de la spécification, et ce que le banc constate pour N = 20. Au-delà, il ne reste qu'une traîne : pour ce flou de 20 pixels, des pixels non blancs jusqu'à 29 pixels du bord, à 0,4 % de noirceur. Pour le padding de la section précédente, c'est l'ordre de grandeur à prévoir.
Étalement et inset : changer la taille et le côté de l'ombre
Le quatrième nombre, l'étalement, agrandit l'ombre (valeur positive) ou la rétrécit (valeur négative) avant le flou. Le mot-clé inset la retourne vers l'intérieur. Trois boîtes de 100 pixels :
.etalement {
box-shadow: 0 0 0 10px #000;
}
.dessous {
box-shadow: 0 12px 8px -8px #000;
}
.interieur {
box-shadow: inset 0 0 0 10px #000;
}Sans flou ni décalage, .etalement donne un anneau net de 10 pixels exactement : les pixels 1 à 10 à gauche de la boîte sont noirs, le 11e est blanc.
.dessous combine un étalement négatif et un décalage vertical pour n'ombrer que le bas, par exemple sous une barre d'en-tête. Mesures : 0 % au 1er pixel au-dessus de la boîte, 1,6 % au 1er pixel à sa gauche, et 63,9 % au 3e pixel sous elle. L'étalement de -8 pixels rétrécit l'ombre pour que son flou (8 pixels) ne déborde presque plus sur les côtés ; avec -6 pixels, le banc relève encore 5,5 % au même endroit. Un étalement négatif dont la valeur absolue est au moins égale au rayon de flou est un bon point de départ.
.interieur dessine l'anneau à l'intérieur de la boîte : pixels 1 à 10 intérieurs noirs, 11e blanc, et le pixel juste à l'extérieur reste blanc. Comme l'ombre extérieure, l'ombre intérieure ne change pas la taille de la boîte.
Plusieurs ombres : la première de la liste passe devant
On peut empiler plusieurs ombres en les séparant par des virgules. La question est de savoir laquelle recouvre l'autre. Le banc pose deux anneaux de couleurs différentes, dans les deux ordres :
.rouge-dessus {
box-shadow: 0 0 0 10px red, 0 0 0 20px blue;
}
.bleu-dessus {
box-shadow: 0 0 0 20px blue, 0 0 0 10px red;
}Sur .rouge-dessus, le 5e pixel à gauche de la boîte est rouge et le 15e est bleu : on voit deux anneaux. Sur .bleu-dessus, les deux pixels sont bleus : l'ombre bleue de 20 pixels, déclarée en premier, recouvre entièrement la rouge. C'est la règle de la spécification : la première ombre de la liste est au-dessus, les suivantes passent derrière.
En pratique, on place donc en tête les ombres petites et nettes, et en fin de liste les grandes ombres diffuses, qui servent de fond. C'est exactement la structure de l'ombre réaliste de la dernière section.
border-shadow n'existe pas : ce que vous cherchiez sans doute
Aucune propriété CSS ne s'appelle border-shadow : le nom ne figure pas dans l'index de toutes les propriétés CSS tenu par le W3C. Le navigateur le confirme à sa façon :
.essai {
border-shadow: 2px 2px red;
}CSS.supports("border-shadow", "2px 2px red"); // false
CSS.supports("box-shadow", "2px 2px red"); // trueLa déclaration est simplement écartée : relue dans la feuille de style, la règle devient .essai { }, et getComputedStyle renvoie none pour box-shadow. L'ombre d'une bordure, c'est tout simplement box-shadow : une ombre extérieure part du bord extérieur de la bordure.
Si ce que vous cherchiez est une bordure qui change d'épaisseur au survol sans faire sauter la page, la réponse est une fausse bordure dessinée en ombre. Le banc compare une vraie bordure et une fausse bordure en ombre, sur deux boutons côte à côte :
.bouton-bordure {
border: 2px solid #4f46e5;
}
.bouton-bordure:hover {
border-width: 4px;
}
.bouton-ombre {
border: 0;
box-shadow: 0 0 0 2px #4f46e5;
}
.bouton-ombre:hover {
box-shadow: 0 0 0 4px #4f46e5;
}Au survol, le bouton bordé passe de 84 à 88 pixels de large et son voisin se décale de 4 pixels. Le bouton à ombre reste à 80 pixels et son voisin ne bouge pas (0 pixel). Le banc fait le même essai avec outline: 2px solid #4f46e5 porté à outline-width: 4px au survol : 0 pixel de décalage aussi. La différence est ailleurs : outline n'a pas de flou et n'accepte pas de liste (CSS.supports("outline", "2px solid red, 4px solid blue") renvoie false), alors que box-shadow permet le flou, les anneaux empilés et l'inset.
Dernier point qui rapproche l'ombre de la bordure : elle suit border-radius. Sur un cercle de 100 pixels (border-radius: 50%) avec box-shadow: 0 0 0 10px #000, le 5e pixel à gauche du cercle, sur son axe, est noir, mais le pixel situé au même écart du coin de la boîte englobante reste blanc : l'anneau est rond, pas carré.
drop-shadow : quand l'ombre doit épouser une forme
box-shadow ombre la boîte, jamais son contenu. Sur une image à fond transparent, comme une icône en triangle, c'est un rectangle qui apparaît. Le filtre drop-shadow(), lui, ombre ce qui est réellement peint. Le banc applique les deux à une même image SVG de 100 pixels contenant un triangle bleu :
.ombre-boite {
box-shadow: 8px 8px 0 #000;
}
.ombre-forme {
filter: drop-shadow(8px 8px 0 #000);
}Au 4e pixel à droite de l'image, dans sa partie haute : noir avec box-shadow (le bord du rectangle), blanc avec drop-shadow. Dans l'image, juste à droite du triangle, dans sa zone transparente : blanc avec box-shadow (l'ombre extérieure est découpée sous la boîte), noir avec drop-shadow (l'ombre du triangle). Le triangle lui-même reste bleu dans les deux cas. La spécification Filter Effects décrit drop-shadow() comme une copie floutée et décalée du masque alpha de l'image : la forme, pas la boîte.
Attention en passant de l'un à l'autre : la même valeur ne donne pas le même flou.
.flou-boite {
box-shadow: 0 0 10px #000;
}
.flou-forme {
filter: drop-shadow(0 0 10px #000);
}À 10 pixels du bord, la noirceur vaut 1,6 % avec box-shadow et 14,1 % avec drop-shadow. La spécification l'explique : pour drop-shadow(), la troisième longueur est directement l'écart-type, pas le rayon de flou. Le banc le vérifie : de 0 à 20 pixels du bord, drop-shadow(0 0 10px) ne s'écarte pas de plus de 1,6 point du box-shadow: 0 0 20px de la section sur le flou. Pour convertir, divisez donc le flou par deux. Autres différences de cette fonction : pas d'étalement, pas d'inset, une seule ombre par fonction.
Assembler une ombre réaliste
Toutes les pièces sont là. Pour imiter une lumière venue d'en haut, je mets un décalage vertical positif et aucun décalage horizontal. L'ombre reste discrète grâce à une couleur noire très transparente, ce que permet le canal alpha (revoir au besoin les couleurs et la transparence en CSS). Et elle superpose plusieurs couches, de la plus nette à la plus diffuse :
.carte {
border-radius: 12px;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.12),
0 4px 8px rgba(0, 0, 0, 0.1),
0 12px 24px rgba(0, 0, 0, 0.08);
}Le navigateur conserve bien les trois couches, dans l'ordre : rgba(0, 0, 0, 0.12) 0px 1px 2px 0px, rgba(0, 0, 0, 0.1) 0px 4px 8px 0px, rgba(0, 0, 0, 0.08) 0px 12px 24px 0px. Sur une carte blanche de 200 pixels sur 100, au 6e pixel autour du bord, le banc mesure une noirceur de 9 % sous la carte, 3,1 % sur le côté et 0,4 % au-dessus. Aucune couche n'est foncée en elle-même : c'est leur somme qui crée ce dégradé plus marqué en bas, comme sous un objet éclairé par le dessus. Dans ma façon de voir, la première couche, petite et nette, dessine le contact avec le support ; la dernière, large et pâle, l'éloignement.
Pour faire varier la hauteur apparente (au survol d'une carte, par exemple), je garde les mêmes trois couches et j'augmente seulement les décalages et les flous des deux dernières. Et si la carte vit dans un conteneur en overflow: hidden, je lui laisse en padding au moins le décalage plus le flou de la plus grande couche, soit 36 pixels sous elle avec cette recette. Le banc confirme la marge : sous la carte, le dernier pixel teinté est à 31 pixels du bord.
Sources consultées
- CSS Backgrounds and Borders Module Level 3, section 6.1, box-shadow, W3C, version du 11 mars 2024, consultée le 5 octobre 2026 (syntaxe, couleur par défaut, flou gaussien d'écart-type égal à la moitié du rayon, découpe sous la boîte, ordre d'empilement, absence d'effet sur la mise en page, rayons de coin) ;
- CSS Overflow Module Level 3, ink overflow, W3C, version du 7 octobre 2025, consultée le 5 octobre 2026 ;
- Filter Effects Module Level 1, drop-shadow(), W3C, Working Draft du 18 décembre 2018, consultée le 5 octobre 2026 ;
- All CSS properties, index du W3C, consulté le 5 octobre 2026 ;
- box-shadow, MDN Web Docs, consulté le 5 octobre 2026.