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

La map générée (rouge = X, vert = Y, gris = neutre)
Un damier vu à travers la lentille

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>

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.

Carte de verre

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

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.