Depuis qu'Apple a remis le « liquid glass » au goût du jour, tout le monde en
veut sur son site. Et presque tout le monde livre la même chose : un
backdrop-filter: blur() avec un fond translucide. C'est du verre
dépoli — joli, mais figé. Le vrai verre liquide fait autre chose : il
courbe la lumière. Ce qui passe derrière se déforme aux bords,
grossit au centre, laisse filer une fine frange colorée là où la courbure est
forte. C'est un effet d'optique, pas un effet de flou.
En refondant notre propre site, nous avons voulu cette réfraction-là — la vraie. Nous l'avons obtenue, mais pas par le chemin prévu : en route, nous avons buté sur un bug d'ancrage de Chromium qu'aucune documentation ne mentionne, mesuré un écart de performance de 1 à 7 sur un simple attribut de filtre, et fini par déplacer toute la réfraction dans un shader WebGL. Ce billet raconte ce chemin, démos manipulables à l'appui — parce qu'un effet d'optique s'explique mal avec des mots. Le déclic initial, lui, revient au bel article « Building glass for the web » de l'équipe design d'Aave, qui nous a mis sur la piste des displacement maps.
Tout part d'une displacement map
Le cœur de l'effet tient en une idée : pour chaque pixel du verre, on ne dessine pas ce qui est derrière lui, mais ce qui est derrière un autre point, légèrement décalé. La direction et l'amplitude de ce décalage sont encodées dans une image : la displacement map. Convention héritée de SVG : le canal rouge pilote le décalage horizontal, le vert le vertical, et la valeur 128 signifie « ne bouge pas ».
Pour une lentille de carte, on veut un centre parfaitement neutre et une poussée qui s'intensifie vers les bords. On calcule pour chaque pixel sa distance au bord d'un rectangle arrondi (un SDF), et on module l'amplitude avec une courbe de puissance :
// profil de lentille : neutre au centre, courbé au bord
const edgeDist = distanceAuBord(x, y); // SDF rectangle arrondi
let m = 0;
if (edgeDist < bande) {
m = Math.pow(1 - edgeDist / bande, courbure);
}
// direction radiale, encodée en rouge/vert autour de 128
data[i] = 128 + (dx / len) * m * 127; // X
data[i + 1] = 128 + (dy / len) * m * 127; // Y
Jouez avec les deux paramètres : la bande (quelle épaisseur du bord est déformée) et la courbure (à quel point la déformation se concentre près du bord). Ce couple bande/courbure définit à lui seul la personnalité de la lentille.
Démo 1 — la displacement map et son effet
Piste n°1 : SVG, backdrop-filter… et le bug qui ruine tout
Le web a un outil taillé pour ça :
feDisplacementMap, un filtre SVG qui applique une displacement map
à n'importe quel contenu. Combiné à backdrop-filter, il devrait
suffire : le navigateur réfracte lui-même tout ce qui passe derrière
l'élément — texte, images, tout le DOM.
Sauf que voilà : dans Chromium, si votre filtre utilise des unités en pourcentages (le réflexe naturel), la displacement map n'est pas ancrée à l'élément. Nous l'avons mesuré au banc de test : la même lentille posée à deux endroits du viewport voit sa zone de courbure se déplacer… ailleurs. La bande de réfraction « flotte » à côté de l'élément, selon sa position dans la page. Inutilisable, et déroutant — l'effet marche « presque », ce qui est pire que pas du tout.
Le contournement que nous avons fini par isoler tient en trois choix précis :
<filter id="lens" filterUnits="userSpaceOnUse"
x="-60" y="-60" width="230" height="230">
<feFlood flood-color="rgb(128,128,128)" result="neutre"/>
<feImage href="map.png" x="0" y="0" width="110" height="110" result="lens"/>
<feComposite in="lens" in2="neutre" operator="over" result="map"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="34"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
filterUnits="userSpaceOnUse"avec lafeImageposée à (0,0) : l'espace de coordonnées s'ancre alors sur l'origine de l'élément — et, bonne surprise, il suit sestransform. Idéal pour un élément qui bouge.- Un
feFloodgris neutre sous la map : tout ce qui déborde de la lentille reste immobile au lieu d'être projeté au hasard. - Une région de filtre serrée : sur notre curseur, passer d'une région de 300×300 à 185×185 pixels a fait bondir le rendu de 35 à 240 images par seconde. Le coût du filtre est proportionnel à sa surface, pas à ce qu'on y voit.
C'est la technique idéale pour un petit élément mobile qui doit réfracter le vrai contenu de la page. Chez nous, c'est le curseur. Ici, une loupe de démonstration — promenez votre souris sur le texte :
Démo 2 — réfracter le vrai DOM
Cette loupe n'est pas une image : c'est un simple div avec
un backdrop-filter correctement ancré. Le texte que vous lisez
en ce moment se courbe réellement à l'intérieur de la lentille, avec une
légère aberration chromatique sur les bords. Aucune capture, aucun canvas :
le navigateur réfracte sa propre page. Si rien ne se déforme, votre
navigateur ne supporte pas les filtres SVG en backdrop-filter (Safari,
Firefox) — c'est précisément pour ça qu'il faut aussi la piste n°2.
Piste n°2 : le fond en WebGL, la méthode qui marche partout
Pour les cartes et boutons — des éléments nombreux, statiques, présents sur tout le site — dépendre d'un comportement non spécifié de Chromium n'était pas acceptable. Aave donne la direction dans son article : pour ses surfaces canvas et vidéo, « the same displacement map feeds a WebGL shader instead ». Nous avons généralisé l'idée : puisque notre fond de page (un quadrillage fin et une constellation de particules) est déjà un canvas, autant faire la réfraction dans le canvas lui-même.
L'architecture : la scène est dessinée dans un canvas 2D hors écran, puis un fragment shader la recopie à l'écran en la courbant sous chaque élément de verre. Pas de map bitmap ici — le SDF est calculé analytiquement par pixel, pour chaque lentille :
float sd = sdRoundRect(p - centre, demiTaille, rayon);
if (sd < 0.0) { // on est sous le verre
float edge = -sd; // distance au bord
if (edge < bande) {
float m = pow(1.0 - edge / bande, courbure);
vec2 disp = normalize(p - centre) * m * deplacement;
couleur = scene(p + disp); // on regarde « à côté »
}
}
Les rectangles des éléments sont relus à chaque frame avec
getBoundingClientRect() : la réfraction suit le défilement, les
animations d'apparition, l'ouverture des accordéons — gratuitement. Seize
lentilles passent dans un uniform array, et le survol devient trivial à animer :
on interpole simplement la profondeur de la lentille survolée, frame après
frame. La géométrie du filtre ne change pas, et ça ne coûte rien.
Démo 3 — le terrain de jeu : la carte est un div transparent, la courbure vient du shader qui déforme la scène derrière elle. Poussez le déplacement à fond pour voir l'effet « goutte d'eau ».
Nota : les sliders ci-dessus pilotent le conteneur de démo ; les cartes du reste de la page utilisent nos réglages de production (bande 12,5 %, courbure 1.8, déplacement 55 px).
L'aberration chromatique, sans l'arc-en-ciel
Le détail qui fait « verre optique », c'est la frange colorée : les longueurs d'onde ne se réfractent pas toutes pareil, le rouge et le bleu se séparent légèrement là où la courbure est forte. En WebGL, trois échantillonnages suffisent :
float r = scene(p + disp * (1.0 + chroma)).r;
float g = scene(p + disp).g;
float b = scene(p + disp * (1.0 - chroma)).b;
Deux leçons apprises à nos dépens. La première : le chroma s'applique
au déplacement, jamais en valeur absolue — sinon la séparation des
couleurs s'étale sur toute la surface au lieu de rester aux bords, et votre
carte devient un arc-en-ciel (nos premiers essais à ±20 % étaient…
mémorables). La seconde, côté SVG : si vous séparez les canaux avec trois
feDisplacementMap, utilisez une échelle unique et trois
maps d'amplitudes différentes. Trois échelles différentes sur la même
map désalignent les canaux sur toute la surface dans Chromium, y compris les
zones optiquement neutres.
Et surtout : n'essayez pas de tricher en peignant un dégradé multicolore dans les bords. La diffraction doit être réelle — visible uniquement là où quelque chose est effectivement courbé — sinon l'œil détecte la supercherie immédiatement.
Les pièges qui ne pardonnent pas
Le canvas Retina. Un <canvas> est un
élément « remplacé » : en position: fixed; inset: 0 sans
width/height CSS explicites, il ne s'étire pas sur le viewport —
il garde sa taille intrinsèque, c'est-à-dire son attribut, qui vaut viewport ×
DPR. Sur un écran classique, les deux coïncident et tout semble parfait. Sur un
écran Retina, le canvas déborde de 2× et toute la réfraction se décale « dans le
vide », à côté des cartes. Le correctif tient en trois règles : des dimensions
CSS explicites, des mesures prises sur
documentElement.clientWidth (et non innerWidth, qui
inclut la barre de défilement — 15 px d'écart), et un
devicePixelRatio revérifié à chaque frame, car déplacer la fenêtre
d'un écran à l'autre ne déclenche pas toujours de resize.
Le verre a besoin de matière. Une lentille sur un fond uni ne montre strictement rien — votre client dira « je ne vois pas l'effet », et il aura raison. C'est pour ça que toutes les bonnes démos de verre posent leur lentille sur une photo ou une grille. Donnez un fond texturé discret à votre page : un quadrillage fin (le nôtre : pas de 44 px, opacité ~5 %) et quelques particules suffisent. Et dessinez ce quadrillage dans le canvas, pas en CSS, sinon il ne sera pas réfracté.
Vérifiez au motif rayé. Notre outil de contrôle favori : injecter temporairement des rayures fines et contrastées derrière le verre. Elles doivent se courber en arcs aux bords et dans les angles, rester parfaitement droites au centre, et porter la frange colorée uniquement sur les zones courbées. Trente secondes de test, zéro ambiguïté.
Accessibilité et replis
prefers-reduced-motion: pas de canvas animé, fond statique.prefers-reduced-transparency: surfaces opaques, pas de curseur en verre.- Curseur personnalisé uniquement si
(pointer: fine)— au tactile, le comportement natif reste intact. - Sans WebGL : la scène s'affiche sans réfraction. Sans filtres SVG en backdrop-filter (Safari, Firefox) : la loupe devient une simple pastille de verre. L'effet est un embellissement, jamais une dépendance.
Ce qu'il faut retenir
Le liquid glass crédible repose sur une vraie réfraction : une displacement map (ou son équivalent analytique en shader), une bande de courbure concentrée aux bords, et un chroma proportionnel au déplacement. Sur le web d'aujourd'hui, cela signifie deux outils complémentaires : le backdrop-filter SVG correctement ancré pour réfracter le vrai DOM sous un petit élément mobile, et le WebGL pour courber un fond riche sous tous les autres — fiable sur tous les navigateurs, à 240 images par seconde.
Tout ce que vous voyez sur ce site — les cartes, les boutons, le curseur si vous êtes sur Chrome — utilise exactement ce qui est décrit ici. Envie du même rendu sur votre site, ou d'une équipe qui creuse les problèmes jusqu'au comportement du moteur de rendu ? Parlons-en.
Cet article détaille le comment. La démarche qui a permis d'y arriver — reproduire au plus petit, isoler une variable à la fois, mesurer avant d'arbitrer — est racontée dans l'étude de cas associée : Un verre qui réfracte vraiment.