Pour filter et backdrop-filter, Lightning CSS fusionne une déclaration préfixée et une non préfixée en une seule, en gardant les drapeaux de préfixe de celle que vous avez écrite en dernier. Donc backdrop-filter suivi de -webkit-backdrop-filter compile en WebKit uniquement. Chrome ne supporte pas du tout -webkit-backdrop-filter, donc le flou disparaît silencieusement en production alors que la source a l'air correcte. La plupart des autres propriétés préfixées ressortent indemnes du même traitement — et c'est exactement ce qui rend celle-ci facile à rater.
Le symptôme
Un header sticky sur l'un de mes templates avait un effet verre dépoli : fond translucide, flou derrière. Ça a marché sur ma machine pendant des semaines. Puis j'ai regardé le site déployé dans Chrome et le header n'était plus qu'un aplat translucide. Aucun flou. Le contenu défilait dessous, parfaitement net.
La source disait le contraire. La règle était bien là dans globals.css, la classe était sur l'élément, aucune media query ne la masquait, et il n'y avait pas de garde @supports. Les DevTools montraient que l'élément correspondait bien à la règle.
Ce que j'ai supposé
J'ai supposé un problème de spécificité CSS, ou un problème de stacking context — backdrop-filter est connu pour être sensible à ce qu'il y a derrière lui et aux ancêtres qui créent des blocs conteneurs. J'ai passé un temps gênant sur transform et filter sur les éléments parents.
C'était faux, et si c'était faux, c'est parce que je lisais la source, pas la feuille de styles que le navigateur avait téléchargée.
Le mécanisme réel
Les templates se construisent avec Next.js 16.2.10 (pinné dans package.json) ; tailwindcss et @tailwindcss/postcss sont déclarés en ^4 et résolvent en 4.3.2 dans mon install. La config PostCSS, c'est juste :
// launch-template/postcss.config.mjs
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;Tailwind v4 compile le CSS via Lightning CSS — lightningcss@1.32.0 dans mon install. Lightning CSS ne traite pas -webkit-backdrop-filter et backdrop-filter comme deux propriétés sans rapport. Il parse les deux en une seule propriété interne qui porte un jeu de drapeaux de préfixe vendeur, puis ré-émet les drapeaux qui ont survécu.
Deux déclarations pour la même propriété, c'est la dernière qui gagne, comme l'exige la cascade. Sauf que ce qui « gagne », c'est la déclaration entière y compris ses drapeaux de préfixe. Écrivez la propriété standard en premier et celle en WebKit en second, et le drapeau standard disparaît.
J'ai lancé le compilateur directement pour confirmer. Le même lightningcss@1.32.0 que celui utilisé par le build :
### Tailwind v4 default targets (safari 16.4, chrome 111, firefox 128)
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); }
standard alone => .c { -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); }Relisez cette dernière ligne attentivement. Écrire uniquement la propriété standard produit les deux. Le compilateur sait déjà que Safari avait besoin du préfixe et l'ajoute. Ma seconde déclaration « ceinture et bretelles » n'ajoutait aucune sécurité — elle détruisait la propriété standard.
C'est bien le dernier drapeau qui gagne, pas une préférence pour WebKit. Sans aucun target configuré, la fusion part dans l'autre sens :
### no targets at all
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { backdrop-filter: blur(16px); }C'est la famille filter, pas toutes les propriétés
Mon premier réflexe a été d'arracher tous les préfixes écrits à la main dans le code. Avant de le faire, j'ai passé la même paire standard-puis-webkit dans le compilateur pour un éventail de propriétés, et le résultat m'a arrêté :
filter => -webkit-filter: blur(4px) ← standard GONE
backdrop-filter => -webkit-backdrop-filter: blur(16px) ← standard GONE
mask-image => both emitted
user-select => both emitted
background-clip => both emitted
hyphens => both emitted
box-decoration-break => both emitted
appearance => appearance: none (collapses to standard)
clip-path, transform => standard only, prefix droppedSeule la famille filter perd la propriété standard. Tout le reste émet soit les deux formes, soit fusionne vers la standard, donc un préfixe redondant y est réellement inoffensif.
filter est sans doute la pire des deux. Avec ces targets, filter: blur(4px) seul compile en filter: blur(4px) — aucun target n'a besoin du préfixe — donc écrire -webkit-filter juste après remplace une déclaration qui marche par une autre qu'aucun navigateur de votre matrice de support n'a demandée.
Cette incohérence est le vrai piège. Vous vérifiez user-select, vous voyez les deux formes en sortie, vous en concluez que vos préfixes défensifs vont bien, et vous ne pensez jamais à vérifier les deux propriétés où ce n'est pas le cas.
Pourquoi Chrome, précisément, ne rend rien
J'avais à moitié supposé que Chrome accepterait -webkit-backdrop-filter comme alias historique, vu que Blink dérive de WebKit. Ce n'est pas le cas. Dans Chrome 151 :
CSS.supports('backdrop-filter', 'blur(16px)') // true
CSS.supports('-webkit-backdrop-filter', 'blur(16px)') // falseLe poser en inline part directement à la poubelle — ça ne se mappe même pas sur la propriété standard. Alors je l'ai sondé sur un vrai élément :
const mk = (css) => { /* build a div, apply css, read computed style */ };
mk('-webkit-backdrop-filter:blur(16px)') // "none"
mk('-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)') // "blur(16px)"
mk('backdrop-filter:blur(16px)') // "blur(16px)""none". La déclaration émise par le compilateur, Chrome l'ignore complètement. Safari continuait de marcher, et c'est exactement pour ça que c'est passé sous les radars aussi longtemps.
Le diagnostic qui tranche
Arrêtez de lire votre source. Lisez les octets que le navigateur a réellement reçus :
# find the stylesheet the deployed page links
curl -sL https://launch.violettadev.com/en | grep -oE '[^"]+\.css' | sort -u
# then count the two forms in it
curl -sL https://launch.violettadev.com/_next/static/chunks/<hash>.css \
| grep -o -- '-webkit-backdrop-filter' | wc -l
curl -sL https://launch.violettadev.com/_next/static/chunks/<hash>.css \
| grep -o -- '[^-]backdrop-filter' | wc -lSi ces deux nombres ne correspondent pas, vous avez des déclarations fusionnées. Sur le build actuel, ils correspondent — 11 et 11. Voici la règle nav telle qu'elle est livrée, verbatim depuis la feuille de styles déployée :
.lv-nav{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}Et voici ce que j'ai écrit pour l'obtenir, depuis launch-template/src/app/globals.css :
.lv-nav {
position: sticky; top: 0; z-index: 100;
height: 64px; display: flex; align-items: center;
border-bottom: 1px solid transparent; transition: all .2s;
background: color-mix(in srgb, var(--bg) 80%, transparent);
backdrop-filter: blur(16px);
}Une déclaration en entrée, deux en sortie. Le compilateur a même préfixé pour moi la liste de propriétés du transition sur la variante overlay : transition:...,-webkit-backdrop-filter .4s,backdrop-filter .4s.
Si vous voulez confirmer la causalité sur une page déjà cassée, faites l'inverse dans les DevTools : sélectionnez l'élément et ajoutez backdrop-filter: blur(16px) à la main dans le panneau Styles. Si le flou apparaît instantanément, le compilateur a mangé votre propriété standard.
Comment j'évite que ça se reproduise
J'ai supprimé les paires backdrop-filter écrites à la main et laissé un commentaire là où la tentation se trouve, dans le globals.css de quatre des cinq templates (formulation identique, remise à la ligne selon chaque fichier) :
/* Do NOT hand-write `-webkit-backdrop-filter` here — or anywhere else in this
file. The build (Tailwind v4 → Lightning CSS) treats the prefixed and
unprefixed property as ONE declaration and keeps only the prefix it saw LAST,
so an authored `backdrop-filter` + `-webkit-backdrop-filter` pair silently
ships as webkit-only and Chrome renders no blur at all. Author the standard
property alone; the build re-adds `-webkit-` when targets need it. */Un commentaire est un rappel, pas un garde-fou. Ce qui attrape vraiment le problème, c'est un script qui scanne le corps des règles à la recherche de toute propriété présente à la fois préfixée et non préfixée dans le même bloc — lancé pour l'instant à la demande plutôt que branché sur la CI. Sur les cinq templates, 933 fichiers CSS/TSX/TS, il trouve zéro paire filter ou backdrop-filter. Ce sont celles-là qui mordent.
Il trouve quand même des paires pour d'autres propriétés : trois paires mask-image et deux paires background-clip dans un template, deux paires user-select et une paire appearance dans un autre. J'ai passé chacune dans le compilateur avant de décider, et je les ai laissées telles quelles — elles émettent toutes les deux formes. Redondant, pas dangereux. Les déclarations -webkit- restantes sont des propriétés qui n'ont aucun équivalent standard : -webkit-font-smoothing, ::-webkit-scrollbar, -webkit-text-size-adjust, ::-webkit-details-marker, -webkit-overflow-scrolling, -webkit-user-drag.
Si vous construisez le même garde-fou, ne le faites pas échouer sur chaque paire — vous enterreriez les deux vrais cas sous une pile de cas inoffensifs. Faites échouer le build sur filter et backdrop-filter ; avertissez pour le reste.
La règle générale, et la partie qui vaut la peine d'être retenue : les pipelines CSS modernes préfixent pour vous, et un préfixe écrit à la main n'est pas une assurance redondante — c'est une déclaration en plus qui peut écraser la vraie. Si vous avez migré vers Tailwind v4 et hérité d'une feuille de styles pleine de préfixes défensifs de 2019, la plupart ne sont que du poids mort, mais ceux de filter et backdrop-filter sont des bugs bien vivants — et ils sont invisibles dans la source, invisibles dans le navigateur sur lequel vous testez, et invisibles pour votre suite de tests. Le seul endroit où ils apparaissent, c'est la feuille de styles que le navigateur a téléchargée.
C'est le pattern que je livre dans launch — démo live sur launch.violettadev.com.