Liquid Glass Refraction
Une plaque de verre qui tord vraiment le décor derrière elle : backdrop-filter enchaîne flou de 8 px, saturation à 180 % et un filtre SVG feDisplacementMap (bruit fractal, décalage jusqu'à 13 px) qui ondule en 22 s, sur quatre taches de couleur en mouvement ; reflet qui suit le pointeur, teintes OKLCH et trois replis (sans flou, mouvement réduit, transparence réduite).
Mis à jour le
Liquid Glass
La lumière se réfracte à travers la surface. Bougez la souris.
Cet effet est gratuit — la catégorie Glassmorphism en contient 47 au total, dont 6 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →
Exemples d'usage
Parc naturel des Hautes-Combes
Cascade du Saut-Gris
- Accès
- Libre · parking 8 h – 20 h
- Sentier
- 1,2 km · 25 min · 90 m de dénivelé
Baignade interdite dans la vasque
Le Phare · bar de l'hôtel · signature
Brise d'Iode
- Gin infusé à la criste marine 4 cl
- Liqueur de sureau 2 cl
- Citron vert, orange sanguine 3 cl
- Tonic, glace taillée top
Servi dès 18 h16 €
Comment ça marche
Trois couches dans une scène en 16/10 (.lgr-scene, coins de 14 px, overflow: hidden, isolation: isolate, fond #0b1024). Dessous, .lgr-bg déborde de 10 % et porte quatre taches rose, bleue, verte et violette (46 % de large, filter: blur(28px)) qui dérivent sur une boucle de 14 s décalée pour chacune. Au-dessus, la plaque .lgr-glass — min(72%, 340px) de large marge comprise (box-sizing: border-box), coins de 26 px — a un fond en deux teintes OKLCH presque blanches à 20 % puis 7 %, un liseré blanc à 28 %, une ombre portée et deux reflets intérieurs. Le cœur est sa ligne backdrop-filter: blur(var(--lgr-blur, 8px)) saturate(180%) url(#lgrRefract) : le décor est flouté, saturé, puis déplacé pixel par pixel par le filtre SVG livré dans le HTML.
Le filtre #lgrRefract fabrique un bruit fractal (feTurbulence, fréquences 0,009 et 0,013, deux octaves, graine 7), l'adoucit d'un flou de 1,4, puis s'en sert comme carte de déplacement (feDisplacementMap, scale="26", canaux rouge et vert) : chaque pixel du fond est décalé jusqu'à 13 px selon la valeur du bruit. Une balise <animate> fait permuter les deux fréquences en 22 s, et la distorsion ondule lentement comme une eau. Sur des rayures nettes posées en fond, la mesure dans Chromium est sans appel : les bandes deviennent des vagues sous la plaque. Le reflet est un ::after en dégradé radial de 120 px, blanc à 45 % en mix-blend-mode: screen, centré sur --mx / --my ; le script les recalcule à chaque pointermove sur la plaque (mesuré : pointeur à 80 % / 70 % de la plaque, --mx à 80 %) et les remet à 30 % / 20 % quand le pointeur sort.
Deux choix du code protègent la page où il est collé. Sous prefers-reduced-motion, le script met en pause la <svg> qui contient le filtre (disp.ownerSVGElement), et pas « la première <svg> de la page » : avec un logo animé posé avant l'effet, c'est le logo qui s'arrêterait et la distorsion qui continuerait (mesuré). Ensuite, Firefox et Safari ignorent la liste backdrop-filter entière dès qu'elle contient url() : mesuré par capture d'écran réelle de Firefox 153 et de WebKit 26 sur des rayures, ni flou, ni saturation, ni réfraction — la plaque ne serait qu'un voile blanc. Un bloc @supports (-moz-appearance: none) or (-webkit-backdrop-filter: blur(1px)), vrai seulement dans ces deux moteurs (mesuré : faux dans Chromium), leur donne le flou et la saturation sans le filtre SVG ; Chromium garde la réfraction. Le script cible des id uniques (#lgrScene, #lgrGlass, #lgrDisp) : une seule plaque par page telle quelle. Une variante plus ancienne de cette vitre liquide existe aussi dans la catégorie.
Accessibilité
- Mouvement réduit : sous
prefers-reduced-motion, les taches s'arrêtent, le reflet ne suit plus le pointeur et descend à 50 % d'opacité, et l'ondulation du filtre est mise en pause (mesuré :animationsPaused()vrai sur la<svg>du filtre, faux sur un logo animé voisin). La plaque reste réfractée, mais figée. - Transparence réduite :
@media (prefers-reduced-transparency: reduce)donne à la plaque un fond plein#14162a, retire le flou et le reflet. Mesuré : la requête répond dans Chromium ; Firefox 153 et WebKit 26 ne connaissent pas encore ce réglage (nireducenino-preferencene répondent), le repli n'y joue donc pas. - Contraste : le texte blanc tient grâce à ses ombres portées (
text-shadownoir à 35 % sur le titre, 55 % sur le paragraphe). Sans elles, mesuré sur la plaque à six instants de l'animation : 2,05:1 en valeur médiane et 1,45:1 sur les zones les plus claires, quand la tache verte passe dessous. Gardez ces ombres, ou foncez la plaque (première teinte OKLCH à 0,4 de luminosité) si le texte s'allonge. - Sémantique : la plaque est un
divavec unh3et un paragraphe, le filtre SVG est enaria-hidden="true". Le reflet ne dépend que du pointeur : au clavier et au toucher rien ne se perd, il reste à sa place par défaut. Si la plaque porte des commandes, utilisez de vrais<button>avec un style:focus-visiblevisible sur le verre.
Compatibilité navigateur
CSS + SVG + 1,2 Ko de JavaScript, aucune dépendance. Mesuré par capture d'écran réelle des fenêtres (les captures automatiques de Firefox et WebKit n'affichent jamais backdrop-filter, même sur un cas témoin) : Chromium applique flou, saturation et réfraction SVG ; Firefox 153 et WebKit 26 appliquent flou et saturation grâce au bloc @supports du code vendu, sans la réfraction — sans ce bloc, ils n'appliqueraient rien du tout. La plaque y reste un verre dépoli lisible, sans l'ondulation. -webkit-backdrop-filter est déclaré pour Safari avant la version 18. Les couleurs OKLCH demandent Chrome 111, Firefox 113, Safari 15.4 ; plus ancien, le fond de la plaque est ignoré et seul le liseré reste.
Sans backdrop-filter du tout, le bloc @supports not donne à la plaque un fond plein à 86 % (rgba(20, 22, 40, .86)) : le texte reste lisible sur les taches. Sans OKLCH, le fond translucide de la plaque disparaît mais le liseré et les ombres restent. Sans JavaScript, le reflet reste fixe à 30 % / 20 % de la plaque et rien d'autre ne change.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<div class="lgr-scene" id="lgrScene">
<div class="lgr-bg">
<span class="lgr-blob lgr-blob--1"></span>
<span class="lgr-blob lgr-blob--2"></span>
<span class="lgr-blob lgr-blob--3"></span>
<span class="lgr-blob lgr-blob--4"></span>
</div>
<div class="lgr-glass" id="lgrGlass">
<h3>Liquid Glass</h3>
<p>La lumière se réfracte à travers la surface. Bougez la souris.</p>
</div>
</div>
<svg width="0" height="0" aria-hidden="true" style="position:absolute">
<filter id="lgrRefract" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.009 0.013" numOctaves="2" seed="7" result="noise">
<animate attributeName="baseFrequency" dur="22s" values="0.009 0.013; 0.013 0.009; 0.009 0.013" repeatCount="indefinite"/>
</feTurbulence>
<feGaussianBlur in="noise" stdDeviation="1.4" result="sturb"/>
<feDisplacementMap id="lgrDisp" in="SourceGraphic" in2="sturb" scale="26" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
.lgr-scene {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
border-radius: 14px;
overflow: hidden;
isolation: isolate;
display: grid;
place-items: center;
background: #0b1024;
}
.lgr-bg {
position: absolute;
inset: -10%;
z-index: 0;
filter: saturate(1.1);
}
.lgr-blob {
position: absolute;
width: 46%;
aspect-ratio: 1;
border-radius: 50%;
filter: blur(28px);
opacity: 0.95;
animation: lgr-float 14s ease-in-out infinite;
}
.lgr-blob--1 {
background: #ff5d8f;
top: -6%;
left: 4%;
}
.lgr-blob--2 {
background: #5d9bff;
bottom: -8%;
right: 2%;
animation-delay: -4s;
}
.lgr-blob--3 {
background: #3ad6b3;
top: 28%;
left: 38%;
animation-delay: -8s;
}
.lgr-blob--4 {
background: #c77dff;
bottom: 10%;
left: 10%;
animation-delay: -11s;
}
.lgr-glass {
position: relative;
z-index: 2;
box-sizing: border-box;
width: min(72%, 340px);
padding: 30px 28px;
border-radius: 26px;
color: #fff;
background: linear-gradient(135deg, oklch(0.98 0.02 255 / 0.20), oklch(0.94 0.04 300 / 0.07));
border: 1px solid rgba(255, 255, 255, 0.28);
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.28),
inset 0 1px 1px rgba(255, 255, 255, 0.55),
inset 0 -2px 6px rgba(255, 255, 255, 0.10);
-webkit-backdrop-filter: blur(var(--lgr-blur, 8px)) saturate(180%) url(#lgrRefract);
backdrop-filter: blur(var(--lgr-blur, 8px)) saturate(180%) url(#lgrRefract);
overflow: hidden;
}
.lgr-glass::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background: radial-gradient(120px 120px at var(--mx, 30%) var(--my, 20%), rgba(255, 255, 255, 0.45), transparent 60%);
mix-blend-mode: screen;
opacity: 0.8;
transition: opacity .3s;
}
.lgr-glass h3 {
margin: 0 0 6px;
font-size: 19px;
letter-spacing: -0.01em;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
}
.lgr-glass p {
margin: 0;
font-size: 13.5px;
line-height: 1.55;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
}
/* Firefox et Safari ignorent TOUTE la liste backdrop-filter dès qu'elle contient url() (mesuré : ni flou ni
saturation). Ces deux moteurs — repérés par une propriété qu'eux seuls connaissent — reçoivent le flou et la
saturation sans la réfraction SVG, que seul Chromium applique. */
@supports (-moz-appearance: none) or (-webkit-backdrop-filter: blur(1px)) {
.lgr-glass {
-webkit-backdrop-filter: blur(var(--lgr-blur, 8px)) saturate(180%);
backdrop-filter: blur(var(--lgr-blur, 8px)) saturate(180%);
}
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.lgr-glass {
background: rgba(20, 22, 40, 0.86);
border-color: rgba(255, 255, 255, 0.35);
}
}
@media (prefers-reduced-motion: reduce) {
.lgr-blob {
animation: none;
}
.lgr-glass::after {
opacity: 0.5;
}
}
@media (prefers-reduced-transparency: reduce) {
.lgr-glass {
background: #14162a;
-webkit-backdrop-filter: none;
backdrop-filter: none;
border-color: rgba(255, 255, 255, 0.4);
}
.lgr-glass::after {
display: none;
}
}
@keyframes lgr-float {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(12%, 8%) scale(1.12);
}
66% {
transform: translate(-8%, -10%) scale(0.94);
}
}
(function() {
/* ============================================================
Sprint juillet 2026 — JS effets glassmorphism (5 IIFE scopées)
============================================================ */
/* fx-0788 : Liquid Glass Refraction */
(function() {
var scene = document.getElementById('lgrScene');
var glass = document.getElementById('lgrGlass');
var disp = document.getElementById('lgrDisp');
if (!scene || !glass) return;
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
glass.addEventListener('pointermove', function(e) {
if (reduceMotion) return;
var r = glass.getBoundingClientRect();
glass.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
glass.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
});
glass.addEventListener('pointerleave', function() {
glass.style.setProperty('--mx', '30%');
glass.style.setProperty('--my', '20%');
});
if (reduceMotion) {
try {
/* la <svg> du filtre, pas la première <svg> de la page */
disp.ownerSVGElement.pauseAnimations();
} catch (e) {}
}
})();
})();
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Force de la réfraction (SVG — <feDisplacementMap scale="26">) |
26 (décalage jusqu'à 13 px) | 10 donne un verre à peine ondulé, 50 un verre de bouteille. Au-delà de 60, le texte du fond devient illisible sous la plaque et des bords apparaissent à la limite du filtre (zone de −20 % à 140 %). |
Taille des ondulations (SVG — <feTurbulence baseFrequency="0.009 0.013">) |
0,009 × 0,013, deux octaves, graine 7 | Plus la fréquence monte, plus les vagues sont petites : 0,03 donne un verre martelé. numOctaves="1" lisse les vagues et allège le calcul. Changez aussi les valeurs de l'<animate>, sinon il les remplace. |
Ondulation animée (SVG — <animate dur="22s">) |
22 s, aller-retour entre les deux fréquences | Supprimez la balise <animate> pour un verre figé, moins coûteux à afficher. dur="8s" donne une eau agitée. |
Flou (CSS — --lgr-blur) |
8px | Posez --lgr-blur: 14px sur .lgr-glass pour un dépoli plus fort. La variable sert aux deux déclarations, celle de Chromium et celle du bloc @supports pour Firefox et Safari. |
Teinte de la plaque (CSS — background: linear-gradient(135deg, oklch(0.98 0.02 255 / .20), oklch(0.94 0.04 300 / .07))) |
blanc bleuté à 20 % vers blanc mauve à 7 % | Pour une plaque teintée : oklch(0.75 0.12 200 / .25) (turquoise). Pour lire un texte long : montez la première opacité à .35 ou baissez la luminosité à 0,4. |
Reflet (CSS — .lgr-glass::after) |
dégradé radial de 120 px, blanc à 45 %, opacité .8 | 180px 180px pour un reflet plus large, opacité .5 pour un reflet discret. Position de repos : les valeurs 30 % / 20 % du pointerleave dans le script. |
Fond derrière le verre (HTML + CSS — .lgr-bg, .lgr-blob) |
4 taches floues qui dérivent en 14 s | Remplacez-les par une image ou une vidéo (object-fit: cover) : la réfraction se voit surtout sur des bords nets, beaucoup moins sur des taches déjà floues. |
FAQ
Pourquoi la plaque n'ondule-t-elle pas dans Firefox ou Safari ?
Parce que ces deux moteurs n'appliquent pas de filtre SVG (url()) dans backdrop-filter, et qu'ils rejettent alors la liste entière, flou compris — mesuré par capture d'écran réelle de Firefox 153 et de WebKit 26 sur un fond rayé : avec url() dans la liste, aucun flou, la plaque n'est qu'un voile blanc. Le code vendu leur donne un bloc à part, repéré par une propriété qu'eux seuls reconnaissent (-moz-appearance pour Firefox, -webkit-backdrop-filter pour Safari), avec le même flou et la même saturation sans le filtre. Le jour où l'un d'eux appliquera url(), il gardera ce verre dépoli : le bloc ne casse rien, il prive seulement de l'ondulation.
Peut-on mettre plusieurs plaques dans la même page ?
Le CSS et le filtre, oui : toutes les plaques .lgr-glass réfractent avec le même #lgrRefract, déclaré une seule fois. Le script, non : il cherche #lgrScene et #lgrGlass par id et ne règle le reflet que de la première plaque. Pour plusieurs plaques, remplacez getElementById par une boucle sur document.querySelectorAll('.lgr-glass') et posez les mêmes écouteurs pointermove / pointerleave sur chacune — c'est ce que font les scènes de cette page.
La scène est coupée en haut et en bas avec le code complet, pourquoi ?
Cela n'arrive que si vous récupérez le code complet avec son cadre de démonstration (par l'API ou le MCP) : le code copié sur cette page ne contient pas ce cadre. Le cadre .demo-preview fait 250 px de haut avec overflow: hidden, et la scène prend toute sa largeur au format 16/10. Mesuré à 784 px de large : la scène mesure 490 px, seule sa bande centrale de 250 px se voit (la plaque reste entière et centrée) ; à 374 px, la scène fait 234 px et deux bandes grises de 8 px apparaissent dessus et dessous. Dans votre page, retirez la hauteur de .demo-preview (ou le cadre entier) et donnez à .lgr-scene la largeur voulue : le format 16/10 fixe alors sa hauteur.