background-clip
fait partie de ces propriétés que je connais depuis des années, mais que j’ai rarement utilisées, peut-être une ou deux fois pour répondre à une question sur Stack Overflow, jusqu’à l’année dernière, où j’ai commencé à créer une collection de barres de défilement. Certains des designs que j’ai reproduits étaient un peu plus complexes et je n’avais qu’un élément disponible par barre : un élément input
. Je ne pouvais donc pas utiliser les pseudo-éléments. Même si cela fonctionne dans certains navigateurs, le fait est que c’est à cause d’un bug et je ne voulais pas me baser là-dessus. En un mot comme en cent, j’ai fini par énormément utiliser les arrière-plans, les bordures et les ombres. J’ai beaucoup appris en faisant cela et cet article partage certaines de ces leçons.
Avant toutes choses, voyons ce à quoi correspond background-clip et ce qu’il fait.
Sur l’image suivante, nous avons un modèle des boîtes pour les éléments :
Si le padding est fixé à 0
, alors la boîte de padding (padding-box
) a exactement la même taille que la boîte de contenu (content-box
) et la limite du contenu coïncide avec la limite du padding.
Si l’épaisseur de la bordure (border-width
) est fixée à 0
, la boîte de la bordure a la même taille que la boîte de padding et la limite de la bordure coïncide avec la limite du padding.
Si, en même temps, le padding est fixé à 0
et l’épaisseur de la bordure à 0
, alors les trois boîtes (contenu, padding et bordure) ont la même taille et les trois limites coïncident.
Par défaut, les arrière-plans recouvrent toute la boîte de la bordure (ils sont même appliqués sous la bordure), mais la position de l’arrière-plan (background-position
) et les tailles relatives pour background-size
sont fixées par rapport à la boîte de padding.
Pour mieux comprendre tout ça, étudions un exemple. On s’intéresse à une boîte avec des dimensions aléatoires, on lui attribue un fond dégradé simple avec background-size: 50% 50%
ainsi qu’une bordure en tirets (en utilisant une border-image
) afin qu’on puisse voir à travers les tirets ce qui se passe sous la bordure.
Dans cette démo, nous pouvons voir que le fond dégradé occupe toute la boîte de bordure : on le voit sous la bordure en tirets. Nous n’avons pas indiqué de position pour le fond, il prend donc la valeur par défaut (0 0
). Nous pouvons nous rendre compte que cette position dépend de la boîte de padding, car elle démarre du coin supérieur gauche (le point de coordonnées 0 0
) de cette boîte. Nous pouvons également voir que la taille définie en pourcentage est relative à la taille de la boîte du padding.
Quand on règle la taille du fond (background-size
) avec un gradient (sans image réelle), nous avons généralement besoin de 2 valeurs pour obtenir des résultats comparables avec les différents navigateurs. Si on ne met qu’une valeur, Firefox fixe la seconde à 100%
(comme indiqué par la spécification), alors que tous les autres navigateurs choisissent, de façon incorrecte, une valeur égale à la première. L’absence de cette seconde valeur est comprise comme étant auto
et, comme les gradients n’ont pas de dimensions ni de proportions intrinsèques, la valeur auto
ne peut pas être déduite de celles-ci, elle doit donc être fixée à 100%
. Ainsi, sauf dans le cas où nous souhaitons que les deux dimensions du fond soient 100%
, nous devons utiliser 2 valeurs.
En utilisant une seule valeur pour la taille du fond, cela ne produit pas de résultats cohérents sur les différents navigateurs. À gauche : Firefox (qui, selon la spécification, fixe la seconde valeur à 100%
) ; à droite : Chrome/Opera, Safari, IE/Edge (qui, de façon incorrecte, choisissent la seconde valeur égale à la première).
Nous pouvons décider que l’arrière-plan recouvre uniquement la boîte de padding ou la boîte de contenu grâce à background-clip
. Le rognage (clipping) consiste à couper et à ne pas afficher ce qui est situé en dehors de la zone de rognage. Cette dernière correspond à la zone à l’intérieur du rectangle en pointillés ci-dessous.
Par défaut, background-clip
vaut border-box
et la zone de rognage est donc la boîte de bordure, ainsi l’arrière-plan est également dessiné en dessous de la bordure.
Avec background-clip: padding-box
, la zone de rognage est la boîte de padding. Cela signifie que l’arrière-plan sera uniquement affiché à l’intérieur de la boîte de padding (il ne sera pas visible sous la bordure).
Enfin, avec background-clip: content-box
, la zone de rognage correspond à la boîte de contenu et l’arrière-plan est seulement visible à l’intérieur de cette boîte.
La démonstration ci-dessous illustre ces trois situations :
Il existe une autre propriété, appelée background-origin
, qui définit à laquelle des trois boîtes se réfère background-position
et background-size
(quand la valeur de cette propriété est exprimée en pourcents).
Imaginons que nous avons un élément comme avant avec une bordure hachurée et cette fois un padding visible. Nous superposons une image réelle avec un dégradé d’arrière-plan. Les deux ont background-size: 50% 50%
sans répétition. De plus, l’image a background-position: 100% 100%
(on laisse le 0 0
par défaut pour le dégradé) :
background: linear-gradient(to right bottom,
#e18728, #be4c39, #9351a6, #4472b9),
url(tiger_lily.jpg) 100% 100%;
background-repeat: no-repeat;
background-size: 50% 50%;
La démonstration qui suit illustre ce qui se passe pour chacune des trois valeurs possibles de background-origin
(border-box
, padding-box
, et content-box
) :
Le couple de valeur 100% 100%
, utilisé dans la déclaration background
, correspond à 100% 100%
de la boîte définie avec background-origin
. Le couple 50% 50%
, utilisé comme valeur de la propriété background-size
, signifie que l’image devra prendre la moitié, en hauteur et en largeur, de la boîte indiquée par background-origin
.
En utilisant la propriété raccourcie background
, background-origin
et background-clip
peuvent être définies, dans cet ordre, à la fin de la déclaration. Étant donné qu’elles prennent chacune une valeur de boîte, si une seule valeur de boîte est fournie, les deux propriétés utiliseront cette boîte. Si deux boîtes sont définies, background-origin
sera basée sur la première et background-clip
utilisera la deuxième. Si aucune valeur de boîte n’est fournie, ces propriétés utiliseront les valeurs par défaut (padding-box
pour background-origin
et border-box
pour background-clip
).
Très bien ! Maintenant, voyons comment tirer parti de tout ça !
Espace transparent entre la bordure et l’arrière-plan
Certains se souviennent peut-être que nous pouvons avoir une bordure semi-transparente avec background-clip
. Mais nous pouvons aussi ajouter de l’espace entre la bordure et la zone d’arrière-plan sans ajouter d’élément. Le moyen le plus simple consiste à avoir un padding en plus d’une bordure, et de mettre aussi background-clip
à content-box
. En faisant ça avec une propriété raccourcie et en utilisant uniquement une valeur de boîte, background-origin
vaudra également content-box
. Pas de problème pour ce cas, il n’y a pas d’effets indésirables.
border: solid .5em #be4c39;
padding: .5em;
background: #e18728 content-box;
Rogner l’arrière-plan avec content-box
l’empêche de dépasser de la boîte de contenu. Au-delà, il n’y a pas d’arrière-plan, on peut donc voir ce qu’il y a derrière cet élément. Si on ajoute une bordure, on voit la bordure entre la limite de padding et la limite de bordure. Si le padding n’est pas de taille nulle, nous avons toujours une zone entre la limite de contenu et la limite de padding.
Voici ce que ça donne en démo :
Pour rendre les choses plus intéressantes, on peut utiliser un filtre drop-shadow()
pour donner un halo jaune à l’ensemble.
Rappels sur les préfixes : dans beaucoup de ressources, j’ai pu voir les préfixes -moz-
et -ms-
utilisés pour les filtres CSS. Erreur à éviter ! Les filtres CSS ne sont plus préfixés dans Firefox depuis leur implémentation (Firefox 34, automne 2014) et maintenant ils sont présents dans Edge avec une préférence, sans préfixe. Les filtres CSS n’ont donc jamais eu besoin d’être préfixés par -moz-
ou -ms-
. Ne les ajoutez pas dans vos feuilles de style, ils ne font qu’embrouiller le code CSS.
On peut aussi obtenir un effet sympa en utilisant un gradient pour chacune des propriétés background-image
et border-image
. On prendra un gradient qui commence par du orange-rouge en haut puis qui s’atténue vers une transparence complète. Vu que seules les couleurs de base sont différentes et que le gradient appliqué sera le même, on crée une fonction Sass :
@function fade($c) {
return linear-gradient($c, rgba($c, 0) 80%);
}
div {
border: solid 0.125em;
border-image: fade(#be4c39) 1;
padding: 0.125em;
background: fade(#e18728) content-box;
}
Voici le résultat dans cette démo :
Cette approche, qui consiste à utiliser le padding pour créer un espace entre l’arrière-plan et la bordure, n’est pas la meilleure, à moins d’avoir seulement un court texte au milieu. S’il y a plus de texte, l’effet n’est pas très joli.
Le problème qu’on rencontre alors est qu’on ne peut pas ajouter d’espace entre le bord de la zone orange et le texte car le padding est déjà utilisé pour l’espace transparent. On pourrait ajouter un autre élément… ou on pourrait utiliser box-shadow
.
box-shadow
est une propriété qui peut prendre 2, 3 ou 4 valeurs de longueurs. La première correspond au décalage horizontal de l’ombre vers la droite, la deuxième valeur correspond au décalage vertical de l’ombre vers le bas, la troisième correspond au rayon de flou (qui détermine la force du flou autour de l’ombre) et la quatrième correspond au rayon d’étalement (qui caractérise la façon dont l’ombre s’étale dans l’ensemble des directions).
Voici une démo interactive pour jouer avec ces valeurs, cliquez sur chacune d’elle et utilisez le curseur qui s’affiche.
Le rayon de flou doit toujours être supérieur ou égal à zéro mais les décalages et le rayon d’étalement peuvent être négatifs. Un décalage négatif permettra simplement de décaler l’ombre dans la direction opposée (vers la gauche et/ou vers le haut) et un rayon d’étalement négatif signifie que l’ombre rétrécira.
Autre chose à noter ici (ça nous servira dans ce cas) : la zone dessinée avec box-shadow
n’est jamais visible sous l’espace occupé par border-box
, même quand cet espace est à moitié transparent.
Si on garde les décalages et le rayon de flou nuls mais qu’on indique un rayon d’étalement positif, on obtiendra quelque chose de semblable à une deuxième bordure de largeur égale dans chaque direction à partir des limites de la bordure réelle et vers l’extérieur.
Il n’est pas nécessaire que la bordure ait la même épaisseur dans les quatre directions. On peut obtenir des épaisseurs différentes en jouant sur les valeurs des décalages et sur celle du rayon d’étalement. La seule contrainte qu’il faut respecter est la suivante : la somme des largeurs des bordures verticales doit être égale à celle des largeurs des bordures verticales. En utilisant plusieurs ombres, on pourrait lever cette restriction si on n’a pas besoin que la bordure soit semi-transparente, mais cette solution serait plus une source de complexité que de simplicité.
Revenons à notre démo, on utilise l’étalement de box-shadow
pour émuler une bordure et la vraie bordure est utilisée pour créer l’espace transparent, on définit background-clip
avec padding-box
et on laisse le padding remplir son rôle.
border: solid 1em transparent;
padding: 1em;
box-shadow: 0 0 0 1em #be4c39;
background: #e18728 padding-box;
Voici ce que ça donne en démo :
On peut aussi simuler une bordure avec une ombre incrustée. Dans ce cas, elle commence à la limite entre la zone de padding et la zone de bordure et elle s’étend vers l’intérieur autant que le définit le rayon d’étalement :
On peut conjuguer plusieurs ombres box-shadow
et on peut donc émuler plusieurs bordures de cette façon. Prenons un exemple avec deux bordures dont l’une est incrustée. Si la vraie bordure de l’élément est non nulle et transparente et que background-clip
vaut padding-box
, on peut tricher et créer une double bordure avec une zone transparente (celle occupée par la vraie zone de bordure) entre les composants intérieurs et extérieurs. Attention, pour compenser l’espace pris par l’ombre incrustée, il faudra augmenter le padding.
border: solid 1em transparent;
padding: 2em; // on passe de 1em à 2em pour compenser la box-shadow "intérieure"
0 0 0 0.25em #be4c39 /* bordure extérieure */,
inset 0 0 0 1em #be4c39 /* bordure intérieure */;
background: #e18728 padding-box;
Vous pouvez utiliser ce Pen pour voir tout ça en action, avec un filtre drop-shadow()
qui ajoute un effet de brillance.
Dessiner une cible aux bords lisses avec un seul élément (pas de pseudo-élément)
Imaginons qu’on veuille dessiner une cible comme celle-ci en ayant comme contrainte de ne devoir utiliser qu’un seul élément et aucun pseudo-élément.
Une première approche consisterait à utiliser un gradient radial qu’on répète (repeating-radial-gradient
) avec une structure comme celle-ci :
Une demi-cible correspondrait donc a 9 unités. Les axes vertical et horizontal de la cible seraient donc composés de 18 unités. Le gradient radial est noir pour la première unité, transparent jusqu’à la troisième, noir à nouveau, etc. On a une répétition à l’œuvre. Seulement, on a uniquement une unité entre 0 et 1. La première fois, on a une région noire et la deuxième fois également. Ensuite on passe de 3 à 5, ce qui donne deux unités ! Pour régler ça, on ne devrait pas commencer à partir de 0 mais à partir de -1, n’est-ce pas ? D’après la spécification, ceci devrait fonctionner :
$unit: 1em;
background: repeating-radial-gradient(
#000 (-$unit), #000 $unit,
transparent $unit, transparent 3*$unit
);
Voici le Pen qui illustre cette idée :
Seulement voilà, IE a une opinion différente…
Heureusement, ça a été corrigé avec Edge, mais si on doit supporter IE, ça continue de poser problème. Pour ça, plutôt que d’utiliser un gradient répété, on peut utiliser un gradient radial classique car il n’y a pas tant de cercles à dessiner. Au final, on a plus de code, mais ça reste acceptable.
background: radial-gradient(
#000 $unit, transparent 0,
transparent 3*$unit, #000 0,
#000 5*$unit, transparent 0,
transparent 7*$unit, #000 0,
#000 9*$unit, transparent 0
);
Voici la démo :
En utilisant cette méthode, on a bien la même distribution sur les différents navigateurs. Cependant, on a un autre problème : les bords des cercles sont loin d’être lisses comme ce qu’on obtenait sous IE, notamment avec Firefox et Chrome !
On pourrait utiliser cette astuce pour créer une transition moins abrupte :
background: radial-gradient(
#000 calc(#{$unit} - 1px),
transparent $unit,
transparent calc(#{3*$unit} - 1px),
#000 3*$unit,
#000 calc(#{5*$unit} - 1px),
transparent 5*$unit,
transparent calc(#{7*$unit} - 1px),
#000 7*$unit,
#000 calc(#{9*$unit} - 1px),
transparent 9*$unit
);
Le résultat sur la démo :
Pour IE, c’est mieux (on avait déjà un résultat satisfaisant), pour Firefox également mais le problème persiste sous Chrome.
Peut-être que les gradients radiaux ne sont pas la meilleure solution pour ce problème. Pourrait-on adapter la solution utilisée précédemment utilisée, avec background-clip
et box-shadow
? On peut utiliser une box-shadow
extérieure pour le cercle extérieur et une autre incrustée pour le cercle intérieur. L’espace intermédiaire est dessiné avec la bordure transparente. On définit également background-clip
sur content-box
et on définit un padding suffisant pour qu’on ait une zone transparente entre le disque central et le cercle intérieur.
border: solid 2*$unit transparent;
padding: 4*$unit;
width: 2*$unit;
height: 2*$unit;
border-radius: 50%;
box-shadow:
0 0 0 2*$unit #000,
inset 0 0 0 2*$unit #000;
background: #000 content-box;
Voilà le résultat dans le Pen, aucun bord rugueux et aucun problème !
Des contrôles réalistes
Cette idée m’est venue lorsque j’ai dû mettre en forme les pistes, les curseurs et, pour les navigateurs non-Webkit, les barres de progression. Pour ces composants, les navigateurs fournissent un ensemble de pseudo-éléments.
Pour les pistes, on peut utiliser -webkit-slider-runnable-track
, -moz-range-track
et -ms-track
. Pour les curseurs, on a -webkit-slider-thumb
, -moz-range-thumb
et -ms-thumb
. Enfin, pour les barres de progression, il y a -moz-range-progress
, -ms-fill-lower
(les deux sont à gauche du curseur) et -ms-fill-upper
(à droite du curseur). Les navigateurs basés sur Webkit ne proposent pas de pseudo-élément qui permettrait de mettre en forme indépendamment la partie avant le curseur et la partie après le curseur.
Les noms des pseudo-éléments sont incohérents et laids mais ce qui est encore plus moche, c’est de ne pas pouvoir lister toutes les versions de navigateurs en même temps pour les mettre en forme de façon uniforme. Quelque chose comme ceci ne fonctionnera pas :
input[type='range']::-webkit-slider-thumb,
input[type='range']::-moz-range-thumb,
input[type='range']::-ms-thumb { /* les styles ici */ }
On doit toujours l’écrire de cette manière :
input[type='range']::-webkit-slider-thumb { /* les styles ici */ }
input[type='range']::-moz-range-thumb { /* les styles ici */ }
input[type='range']::-ms-thumb { /* les styles ici */ }
Cela donne l’impression d’un style de code très LOURD et c’est vraiment souvent le cas ; cela dit, vu le nombre d’incohérences des navigateurs pour ce qui est des curseurs, c’est parfois utile de pouvoir harmoniser les choses. Pour répondre à ce problème, j’ai utilisé un mixin thumb()
et je lui fournis des arguments pour gérer les incohérences :
@mixin thumb($flag: false) {
/* styles */
@if $flag { /* plus de styles */ }}
input[type='range'] {
&::-webkit-slider-thumb { @include thumb(true); }
&::-moz-range-thumb { @include thumb(); }
&::-ms-thumb { @include thumb(); }}
Revenons à la façon dont on peut mettre en forme les choses. On peut ajouter des pseudo-éléments à ces composants seulement pour Chrome et Opera, ce qui signifie que, pour reproduire leur apparence, on doit s’en approcher le plus possible sans avoir recours aux pseudo-éléments. Il faut donc gérer les arrière-plans, les bordures, les ombres, les filtres sur l’élément.
Voyons quelques exemples !
Un bouton à l’aspect plastique
Pour prendre un exemple visuel, dites-vous qu’on souhaite obtenir un curseur comme celui-ci
À première vue, pour ce type de sélecteur, il devrait suffire d’ajouter un dégradé avec background
, un autre avec border-image
, il n’y a plus qu’à ajouter une box-shadow
et c’est bon :
border: solid 0.375em;
border-image: linear-gradient(#fdfdfd, #c4c4c4) 1;
box-shadow: 0 0.375em 0.5em -0.125em #808080;
background: linear-gradient(#c5c5c5, #efefef);
Effectivement, ça fonctionne (en utilisant un élément button
à la place du curseur pour simplifier les choses) :
Mais notre sélecteur est rond et non carré, il manque donc seulement un border-radius: 50%
et c’est bon ? Eh bien… ça ne fonctionne pas parce que nous utilisons border-image
ce qui fait que border-radius
est ignoré sur l’élément lui-même, même si, chose étrange, il reste appliqué sur box-shadow
s’il y en a un.
Alors que devons-nous faire ? Utiliser background-clip
! Premièrement on donne un padding non nul à l’élément, pas de bordure et on le rend rond avec border-radius: 50%
. Ensuite on superpose les arrière-plans dégradés, celui du dessus étant ajusté à la content-box
(notez que le clipping est inclus dans la propriété raccourcie background
). Enfin on ajoute deux box-shadows
, la première étant plus sombre pour créer l’ombre sous le curseur et la seconde étant incrustée et qui devrait foncer un peu le bas et les côtés de la partie externe du bouton.
border: none; /* pour border-box ≡ padding-box */
padding: .375em;
border-radius: 50%;
box-shadow: 0 .375em .5em -.125em #808080,
inset 0 -.25em .5em -.125em #bbb;
background:
linear-gradient(#c5c5c5, #efefef) content-box,
linear-gradient(#fdfdfd, #c4c4c4);
Le résultat final est visible sur ce Pen :
Un curseur mat
L’objectif à atteindre est de réaliser ce type de curseur:
Cet exemple ressemble au précédent mais on a un contour plus clair en haut et une ombre au milieu du cercle. Si on utilise box-shadow
pour la partie plus claire du contour, on n’aurait plus un rond à moins de réduire la hauteur pour compenser l’ombre. Cela signifie qu’il faudra calculer comment positionner le cercle intérieur. Si on utilise une ombre incrustée, on ne pourra pas en tirer parti pour l’ombre sombre de la partie intérieure. Cependant, on pourrait utiliser une stratégie analogue au cas précédent, sauf qu’on a radial-gradient
sur les autres arrière-plans utilisés. La taille réelle de background-size
pour le gradient est supérieure à la boîte de contenu et on peut donc la décaler vers le bas sans que le bord haut atteigne la limite du contenu.
border: none; /* pour border-box ≡ padding-box */
padding: .625em;
width: 1.75em;
height: 1.75em;
border-radius: 50%;
box-shadow:
0 1px .125em #444 /* ombre inférieure */,
inset 0 1px .125em #fff /* haut plus clair */;
background:
/* effet pour l'ombre intérieure */
radial-gradient(transparent 35%, #444)
50% calc(50% + .125em) content-box,
/* arrière-plan intérieur */
linear-gradient(#bbb, #bbb) content-box,
/* arrière-plan extérieur */
linear-gradient(#d0d3d5, #d2d5d7);
background-size:
175% 175% /* on rend le radial-gradient plus grand */,
100% 100%, 100% 100%;
Voici la démonstration du résultat obtenu
Un contrôle en 3D
Prenons par exemple le bouton de ce curseur :
Celui-ci est plus complexe et il faut que les trois boîtes (contenu, padding, bordure) soient différentes afin qu’on puisse empiler différents arrière-plans et utiliser background-clip
pour obtenir l’effet voulu.
Ainsi pour la partie principale du curseur, on applique un arrière-plan en gradient, fixé par rapport à la boîte de contenu. En dessous on applique un autre arrière-plan, fixé par rapport à la boîte de padding. Encore en dessous, on en place un par rapport à la boîte de bordure avec un gradient linéaire. On utilise également box-shadow
avec inset
pour appuyer sur la limite entre la boîte de padding et la boîte de bordure.
border: solid .25em transparent;
padding: .25em;
border-radius: 1.375em;
box-shadow:
inset 0 1px 1px rgba(#f7f7f7, .875) /* haut */,
inset 0 -1px 1px rgba(#bbb, .75) /* bas */;
background:
linear-gradient(#9ea1a6, #fdfdfe) content-box,
linear-gradient(#fff, #9c9fa4) padding-box,
linear-gradient(#eee, #a4a7ab) border-box;
Voici un démonstration du résultat qu’on obtient :
Maintenant, voyons la partie ronde. Pour Blink, cette fois, j’ai dû me résoudre à utiliser un pseudo-élément. En revanche pour les autres navigateurs, j’ai pu obtenir un résultat satisfaisant en appliquant deux gradients radiaux sur les gradients linéaires :
On pourrait encore aller plus loin pour obtenir un meilleur rendu en ajoutant d’autres gradients ou en utilisant background-blend-mode
mais je n’ai pas suffisamment la fibre artistique pour ça.
Avec un pseudo-élément, c’est beaucoup plus simple d’obtenir le résultat voulu : on fixe sa position et sa taille puis on l’arrondit avec border-radius: 50%
. Ensuite on lui applique un padding, aucune bordure et on utilise deux gradients pour l’arrière-plan dont le plus haut est un gradient radial fixé à la boîte de contenu.
padding: .125em;
background:
radial-gradient(circle at 50% 10%,
#f7f8fa, #9a9b9f) content-box,
linear-gradient(#ddd, #bbb);
Voici le résultat qu’on obtient avec cette méthode :
Pour le curseur réel, j’ai utilisé le même arrière-plan avec les gradients radiaux sur le dessus et j’ai ajouté le pseudo-élément pour Blink sur ces gradients. J’ai utilisé cette méthode car, pour Safari, les styles liés aux curseurs sont appliqués avec ::-webkit-slider-thumb
mais Safari ne prend pas en charge les pseudo-éléments sur le curseur ou la piste. Si je devais retirer ces méthodes de contournement aux styles appliqués à ::-webkit-slider-thumb
, Safari n’afficherait pas du tout la partie ronde.
Une illusion de profondeur
La piste utilisée pour ce curseur illustre bien l’idée voulue :
Comme avant, pour l’élément, on donne une bordure transparente non nulle, un padding et des arrière-plans fixés sur différentes boîtes avec background-clip
(celui avec content-box
doit être au-dessus de celui avec pading-box
qui doit être au-dessus de celui avec border-box
). Dans ce cas, on utilise un gradient linéaire plus clair pour couvrir la zone de bordure, un autre plus sombre et quelques gradients radiaux qu’on réduit et qu’on ne répète pas afin d’assombrir la zone de remplissage et on utilise un gradient encore plus foncé pour la zone de contenu. Ensuite, on définit un border-radius
au moins égal à la moitié de la zone de contenu plus deux fois le padding plus deux fois la bordure. On ajoute également une ombre box-shadow
avec inset
pour mettre en avant la limite du padding.
border: solid .375em transparent;
padding: 1em 3em;
width: 15.75em;
height: 1.75em;
border-radius: 2.25em;
background:
linear-gradient(#090909, #090909) content-box,
radial-gradient(at 5% 40%, #0b0b0b, transparent 70%)
no-repeat 0 35% padding-box /* gauche */,
radial-gradient(at 95% 40%, #111, transparent 70%)
no-repeat 100% 35% padding-box /* droit */,
linear-gradient(90deg, #3a3a3a, #161616) padding-box,
linear-gradient(90deg, #2b2d2c, #2a2c2b) border-box;
background-size: 100%, 9em 4.5em, 4.5em 4.5em, 100%, 100%;
Seulement voilà, on a un problème :
En effet, vu le fonctionnement de border-radius
(le rayon pour la zone de contenu est celui qu’on définit moins border-width
moins le padding, ce qui est négatif), il n’y a pas d’arrondi pour la zone de contenu. Réglons le problème en émulant la forme souhaitée avec des gradients radiaux et linéaires pour la zone de contenu.
Pour ce faire, on commence par s’assurer que la largeur de la zone de contenu est un multiple de sa hauteur (ici 15.75em = 9 x 1.75em
). On applique un gradient linéaire sans répétition, positionné au milieu, qui couvre toute la hauteur de la zone de contenu mais qui laisse des espaces équivalents à la moitié de la hauteur de la zone de contenu aux extrémités droite et gauche. Là-dessus, on ajoute un gradient radial pour lequel background-size
est égal à hauteur de la zone de contenu, horizontalement et verticalement.
Des contrôles métalliques
Comment obtenir un bouton comme celui-ci ?
C’est un peu compliqué à réaliser donc décomposons le problème étape par étape. Pour commencer, on crée un bouton circulaire avec une largeur égale à la hauteur et en utilisant border-radius: 50%
. Ensuite, on s’assure que box-sizing
vaut border-box
afin que le padding aille vers l’intérieur (afin qu’il soit soustrait aux dimensions que nous avons définies). Ensuite, on crée un bordure transparente et un padding. Voilà ce que ça donne :
$d-btn: 27em; /* le diamètre */
$bw: 1.5em; /* la largeur de la bordure */
button {
box-sizing: border-box;
border: solid $bw transparent;
padding: 1.5em;
width: $d-btn; height: $d-btn;
border-radius: 50%;
}
Ça ne ressemble pas encore à grand-chose car l’effet est principalement accompli avec deux propriétés encore absentes : box-shadow
et background
.
Avant d’aller plus loin, décomposons l’ensemble :
En partant de l’extérieur vers l’intérieur, on a :
- Un anneau avec des diodes
- Un anneau fin
- Une zone perforée (qui contient le halo bleu autour de la zone intérieure
- Une partie centrale ronde.
L’anneau extérieur sera la zone de la bordure, la zone centrale sera la zone de contenu et toute la partie intermédiaire (l’anneau intérieure et la partie perforée) sera la zone de padding. On crée l’anneau intérieur avec des ombres box-shadow
en inset
.
box-shadow:
/* Un ombre discrète pour séparer l'anneau extérieur */
inset 0 0 1px #666,
/* La zone haute plus sombre */
inset 0 1px .125em #8b8b8b,
inset 0 2px .25em #a4a2a3,
/* La zone basse plus sombre */
inset 0 -1px .125em #8b8b8b,
inset 0 -2px .25em #a4a2a3,
/* La bande circulaire pour l'anneau intérieur */
inset 0 0 0 .375em #cdcdcd;
On ajoute deux autres ombres extérieures, une première pour la marque avant l’anneau extérieur et une seconde pour l’ombre discrète sous le contrôle, on a donc :
box-shadow:
0 -1px 1px #eee,
0 2px 2px #1d1d1d,
inset 0 0 1px #666,
inset 0 1px .125em #8b8b8b,
inset 0 2px .25em #a4a2a3,
inset 0 -1px .125em #8b8b8b,
inset 0 -2px .25em #a4a2a3,
inset 0 0 0 .375em #cdcdcd;
On n’y est pas encore tout à fait mais ça avance :
On doit maintenant ajouter trois types d’arrière-plan en partant du haut vers le bas : ceux restreints à la boîte content-box
(pour la zone centrale), ceux restreints à la boîte de padding (pour la zone perforée et le halo bleu) et ceux restreints à la boîte de bordure (pour créer l’anneau extérieur et les diodes).
On commence par la zone centrale où on a des lignes circulaires qui sont créées avec les trois gradients radiaux empilés qui créent un effet de texture avec cet empilement et qui permettent d’obtenir des réflexions coniques avec un gradient conique. Aujourd’hui, les gradients coniques ne sont pas pris en charge par tous les navigateurs et il faudra donc utiliser une prothèse (polyfill).
background:
/* ======= content-box ======= */
/* les lignes circulaires - 13, 19, 23 sont premiers */
repeating-radial-gradient(
rgba(#e4e4e4, 0) 0,
rgba(#e4e4e4, 0) 23px,
rgba(#e4e4e4, .05) 25px,
rgba(#e4e4e4, 0) 27px) content-box,
repeating-radial-gradient(
rgba(#a6a6a6, 0) 0,
rgba(#a6a6a6, 0) 13px,
rgba(#a6a6a6, .05) 15px,
rgba(#a6a6a6, 0) 17px) content-box,
repeating-radial-gradient(
rgba(#8b8b8b, 0) 0,
rgba(#8b8b8b, 0) 19px,
rgba(#8b8b8b, .05) 21px,
rgba(#8b8b8b, 0) 23px) content-box,
/* réflexions coniques */
conic-gradient(/* des variations aléatoires sur des tons gris */
#cdcdcd, #9d9d9d, #808080,
#bcbcbc, #c4c4c4, #e6e6e6,
#dddddd, #a1a1a1, #7f7f7f,
#8b8b8b, #bfbfbf, #e3e3e3,
#d2d2d2, #a6a6a6, #858585,
#8d8d8d, #c0c0c0, #e5e5e5,
#d6d6d6, #9e9e9e, #828282,
#8f8f8f, #bdbdbd, #e3e3e3, #cdcdcd)
content-box;
Ça commence à ressembler à quelque chose !
Passons à la partie perforée. Le halo bleu est simplement un gradient radial transparent à l’extérieur. Les perforations utilisées sont tirées du motif « fibre de carbone » provenant de la galerie construite par Lea Verou il y a cinq ans.
$d-hole: 1.25em; /* le diamètre d'une perforation */
$r-hole: .5*$d-hole; /* le rayon d'une perforation */
background:
/* ======= padding-box ======= */
/* le halo bleu */
radial-gradient(
#00d7ff 53%, transparent 65%) padding-box,
/* les trous */
radial-gradient(
#272727 20%, transparent 25%)
0 0 / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(
#272727 20%, transparent 25%)
$r-hole $r-hole / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(#444 20%, transparent 28%)
0 .125em / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(#444 20%, #3d3d3d 28%)
#{$r-hole} #{$r-hole + .125em} / #{$d-hole} #{$d-hole}
padding-box
On s’approche d’un résultat satisfaisant :
Pour créer l’anneau extérieur, on utilise simplement un gradient conique :
conic-gradient(
#b5b5b5, #8d8d8d, #838383,
#ababab, #d7d7d7, #e3e3e3,
#aeaeae, #8f8f8f, #878787,
#acacac, #d7d7d7, #dddddd,
#b8b8b8, #8e8e8e, #848484,
#a6a6a6, #d8d8d8, #e3e3e3,
#8e8e8e, #868686, #a8a8a8,
#d5d5d5, #dedede, #b5b5b5) border-box;
Et voilà un contrôle métallique !
Il n’y a pas encore de diode, réglons ce problème !
Chaque diode est composée de deux gradients radiaux empilés. Celui du dessus modélise la diode et celui du dessous, légèrement décalé, crée une légère marque sous la diode afin d’obtenir le même effet que pour les trous de la zone perforée. Le gradient du dessous est toujours le même, en revanche, celui du dessus varie selon l’état de la diode (allumée ou non).
On prend la variable $k
pour indiquer le nombre de diodes allumées et on utilise un gradient bleu pour celles-ci et un gradient gris pour les autres.
On a 24 diodes positionnées sur un cercle qui est au milieu de la zone de bordure. Le rayon du cercle est égal au rayon du contrôle auquel on a soustrait la moitié de la largeur de la bordure.
On utilisera Sass pour créer tous ces gradients. Pour commencer on crée une liste vide pour les gradients et à chaque itération de la boucle, on ajoute deux gradients à la liste. Les positions sont calculées afin qu’elles soient sur le cercle précédent. Le premier gradient dépend de l’indice de la boucle et le deuxième est toujours le même (seule sa position change).
$d-btn: 27em;
$bw: 1.5em;
$r-pos: .5*($d-btn - $bw);
$n-leds: 24;
$ba-led: 360deg/$n-leds;
$d-led: 1em;
$r-led: .5*$d-led;
$k: 7;$leds: ();
@for $i from 0 to $n-leds {
$a: $i*$ba-led - 90deg;
$x: .5*$d-btn + $r-pos*cos($a) - $r-led;
$y: .5*$d-btn + $r-pos*sin($a) - $r-led;
$leds: $leds,
if($i < $k,
(radial-gradient(circle, #01d6ff,
#178b98 .5*$r-led,
rgba(#01d6ff, .35) .7*$r-led,
rgba(#01d6ff, 0) 1.3*$r-led) no-repeat
#{$x - $r-led} #{$y - $r-led} /
#{2*$d-led} #{2*$d-led} border-box),
(radial-gradient(circle, #898989,
#4d4d4d .5*$r-led, #999 .65*$r-led,
rgba(#999, 0) .7*$r-led) no-repeat
$x $y / #{$d-led} #{$d-led} border-box)
),
radial-gradient(circle,
rgba(#e8e8e8, .5) .5*$r-led,
rgba(#e8e8e8, 0) .7*$r-led) no-repeat
$x ($y + .125em) / #{$d-led} #{$d-led}
border-box;
}
Voici le résultat final sur CodePen :
Afficher des ombres sur un plan perpendiculaire
Prenons le cas où des contrôles sont affichés sur le plan, vertical, de l’écran et pour lesquels on voudrait un ombre portée sur le plan horizontal en dessous, quelque chose qui ressemble à cette image :
On souhaite recréer cet effet avec un seul élément et sans pseudo-élément.
Là aussi, on peut empiler différents arrière-plans avec différentes valeurs de background-clip
et background-origin
. On crée le bouton avec deux arrière-plans, le premier en haut, attaché à la boîte de contenu et celui du dessous, attaché à la boîte de padding, pour créer l’ombre on utilise un arrière-plan radial-gradient()
avec background-clip
et background-origin
fixés à border-box
pour créer l’ombre.
La mise en forme de base est obtenue de façon semblable au contrôle de la section précédente :
$l: 6.25em;
$bw: .1*$l;
border: solid $bw transparent;
padding: 3px;
width: $l;
height: $l;
border-radius: 1.75*$bw;
On ajoute un bordure épaisse transparente tout autour afin d’avoir suffisamment d’espace pour recréer l’ombre dans la partie basse de la zone de bordure. On applique cela à toutes les bordures (pas uniquement à celle du bas) pour obtenir le même effet d’arrondi symétrique sur tous les coins de la boîte de padding (s’il vous faut des rappels sur le sujet, n’hésitez pas à consulter l’excellente conférence de Lea Verou sur border-radius
).
Le premier arrière-plan, en partant du dessus, est un gradient conique qui crée des réflexions métalliques coniques. Celui-ci est rattaché à la boîte de contenu avec content-box
. Juste en dessous, on a un gradient linéaire rattaché à la boîte de padding. On utilise trois ombres vers l’intérieur pour que ce deuxième arrière-plan semble moins plat (on ajoute une autre nuance toute autour avec un flou nul et qui s’étend légèrement, on rend le haut plus clair avec une ombre blanche semi-transparente et on assombrit la partie basse avec une ombre semi-transparente noire).
box-shadow:
inset 0 0 0 1px #eedc00,
inset 0 1px 2px rgba(#fff, .5),
inset 0 -1px 2px rgba(#000, .5);
background:
conic-gradient(
#edc800, #e3b600, #f3cf00, #ffe800,
#ffe900, #ffeb00, #ffe000, #ebc500,
#e0b100, #f1cc00, #fcdc00, #ffe500,
#fad900, #eec200, #e7b900, #f7d300,
#ffe800, #ffe300, #f5d100, #e6b900,
#e3b600, #f4d000, #ffe400, #ebc600,
#e3b600, #f6d500, #ffe900, #ffe90a,
#edc800) content-box,
linear-gradient(#f6d600, #f6d600) padding-box
Voilà le bouton métallique (il n’y a pas encore l’ombre) :
Pour l’ombre, on applique un troisième couche d’arrière-plan pour laquelle background-clip
et background-origin
sont liés à border-box
. L’arrière-plan est un gradient radial (dont la position est attachée en bas et au milieu) qu’on réduit verticalement afin qu’il puisse tenir dans la zone en bas de la boîte de bordure. Pour ça, on prend un taille environ égale à 75% de border-width
.
radial-gradient(rgba(#787878, .9), rgba(#787878, 0) 70%)
50% bottom / 80% .75*$bw no-repeat border-box
Et voilà, vous pouvez utiliser ces boutons sur cette démo :
background-clip
s’avère utile dans de nombreux cas ! Tout particulièrement lorsqu’on empile différents effets sur les bords des éléments.
Merci beaucoup à Ana Tudor qui a permis cette traduction !