Per filter e backdrop-filter, Lightning CSS fonde la dichiarazione con prefisso e quella senza in una sola, tenendo i flag di prefisso di quella che hai scritto per ultima. Quindi backdrop-filter seguito da -webkit-backdrop-filter compila in solo-WebKit. Chrome non supporta affatto -webkit-backdrop-filter, quindi il blur sparisce in silenzio in produzione mentre il sorgente sembra corretto. La maggior parte delle altre proprietà con prefisso esce indenne dallo stesso trattamento — ed è proprio questo che rende facile non accorgersene.
Il sintomo
Un header sticky di uno dei miei template aveva un effetto vetro smerigliato: sfondo traslucido, blur dietro. Ha funzionato sulla mia macchina per settimane. Poi ho guardato il sito deployato in Chrome e l'header era solo una superficie traslucida piatta. Nessun blur. Il contenuto ci scorreva sotto perfettamente nitido.
Il sorgente diceva altro. La regola era lì in globals.css, la classe era sull'elemento, nessuna media query la nascondeva, e non c'era nessun gate @supports. I DevTools mostravano che l'elemento faceva match con la regola.
Cosa avevo dato per scontato
Ho pensato a un problema di specificità CSS, o a un problema di stacking context — backdrop-filter è notoriamente sensibile a quello che ha dietro e agli antenati che creano containing block. Ho perso un tempo imbarazzante su transform e filter sugli elementi genitori.
Era sbagliato, e il motivo per cui era sbagliato è che stavo leggendo il sorgente, non il foglio di stile che il browser aveva scaricato.
Il meccanismo reale
I template si costruiscono con Next.js 16.2.10 (pinnato in package.json); tailwindcss e @tailwindcss/postcss sono dichiarati come ^4 e risolvono a 4.3.2 nella mia install. La config PostCSS è solo questa:
// launch-template/postcss.config.mjs
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;Tailwind v4 compila il CSS attraverso Lightning CSS — lightningcss@1.32.0 nella mia install. Lightning CSS non tratta -webkit-backdrop-filter e backdrop-filter come due proprietà scollegate. Le fa il parsing entrambe in un'unica proprietà interna che porta con sé un insieme di flag di prefisso vendor, e poi riemette i flag che sopravvivono.
Due dichiarazioni per la stessa proprietà significano che vince l'ultima, come impone la cascata. Solo che la cosa che "vince" è l'intera dichiarazione compresi i suoi flag di prefisso. Scrivi prima la proprietà standard e poi quella WebKit, e il flag standard sparisce.
Ho lanciato il compilatore direttamente per confermarlo. Lo stesso lightningcss@1.32.0 che usa il 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); }Leggi bene quest'ultima riga. Scrivere solo la proprietà standard produce entrambe. Il compilatore sa già che Safari aveva bisogno del prefisso e lo aggiunge. La mia seconda dichiarazione "per sicurezza" non aggiungeva sicurezza — distruggeva la proprietà standard.
È davvero vince-l'ultimo-flag, non una preferenza per WebKit. Senza nessun target configurato, la fusione va nella direzione opposta:
### no targets at all
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { backdrop-filter: blur(16px); }È la famiglia filter, non ogni proprietà
Il mio primo istinto è stato strappare via ogni prefisso scritto a mano nel codice. Prima di farlo ho dato in pasto al compilatore la stessa coppia standard-poi-webkit per una serie di proprietà, e il risultato mi ha fermato:
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 droppedSolo la famiglia filter perde la proprietà standard. Tutto il resto o emette entrambe le forme o collassa verso quella standard, quindi lì un prefisso ridondante è davvero innocuo.
filter è probabilmente la più cattiva delle due. Con questi target filter: blur(4px) da solo compila in filter: blur(4px) — nessun target ha bisogno del prefisso — quindi scriverci sotto -webkit-filter sostituisce una dichiarazione funzionante con una che nessun browser della tua matrice di supporto ha chiesto.
Questa incoerenza è la vera trappola. Controlli user-select, vedi entrambe le forme nell'output, concludi che i tuoi prefissi difensivi vanno bene, e non ti viene mai in mente di controllare le due proprietà dove invece non vanno bene.
Perché è proprio Chrome a non renderizzare nulla
Avevo mezzo dato per scontato che Chrome accettasse -webkit-backdrop-filter come alias legacy, dato che Blink deriva da WebKit. Non è così. In Chrome 151:
CSS.supports('backdrop-filter', 'blur(16px)') // true
CSS.supports('-webkit-backdrop-filter', 'blur(16px)') // falseImpostarla inline finisce nel nulla — non viene nemmeno mappata sulla proprietà standard. Così l'ho sondata su un elemento vero:
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 dichiarazione emessa dal compilatore è una che Chrome ignora completamente. Safari ha continuato a funzionare, ed è esattamente il motivo per cui è sopravvissuta alla review così a lungo.
La diagnosi che chiude la questione
Smetti di leggere il tuo sorgente. Leggi i byte che il browser ha ricevuto davvero:
# 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 -lSe questi due numeri non coincidono, hai dichiarazioni collassate. Sul build attuale coincidono — 11 e 11. Ecco la regola della nav che va in produzione, verbatim dal foglio di stile deployato:
.lv-nav{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}Ed ecco cosa ho scritto per ottenerla, da 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);
}Una dichiarazione in ingresso, due in uscita. Il compilatore mi ha perfino messo il prefisso nella lista di proprietà di transition sulla variante overlay: transition:...,-webkit-backdrop-filter .4s,backdrop-filter .4s.
Se vuoi confermare il nesso causale su una pagina già rotta, fai l'inverso nei DevTools: seleziona l'elemento e aggiungi backdrop-filter: blur(16px) a mano nel pannello Styles. Se il blur compare all'istante, il compilatore si è mangiato la tua proprietà standard.
Come evito che risucceda
Ho rimosso le coppie backdrop-filter scritte a mano e ho lasciato un commento dove sta la tentazione, nel globals.css di quattro dei cinque template (testo identico, mandato a capo per stare in ogni file):
/* 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 commento è un promemoria, non una guardia. Quello che becca davvero il problema è uno script che scansiona il corpo delle regole cercando qualsiasi proprietà presente sia con prefisso sia senza nello stesso blocco — per ora lanciato a richiesta invece che collegato alla CI. Su tutti e cinque i template, 933 file CSS/TSX/TS, trova zero coppie filter o backdrop-filter. Sono quelle che mordono.
Trova comunque coppie per altre proprietà: tre coppie mask-image e due coppie background-clip in un template, due coppie user-select e una coppia appearance in un altro. Le ho passate una per una nel compilatore prima di decidere, e le ho lasciate stare — emettono tutte entrambe le forme. Ridondanti, non pericolose. Le dichiarazioni -webkit- rimaste sono proprietà che non hanno nessun equivalente standard: -webkit-font-smoothing, ::-webkit-scrollbar, -webkit-text-size-adjust, ::-webkit-details-marker, -webkit-overflow-scrolling, -webkit-user-drag.
Se ti costruisci la stessa guardia, non farla fallire su ogni coppia — seppelliresti i due casi veri sotto una pila di casi innocui. Fai fallire il build su filter e backdrop-filter; per il resto, un warning.
La regola generale, e la parte che vale la pena tenersi: le pipeline CSS moderne mettono i prefissi al posto tuo, e un prefisso scritto a mano non è un'assicurazione ridondante — è una dichiarazione in più che può sovrascrivere quella vera. Se hai migrato a Tailwind v4 e ti sei portato dietro un foglio di stile pieno di prefissi difensivi del 2019, la maggior parte è solo peso morto, ma quelli di filter e backdrop-filter sono bug vivi — e sono invisibili nel sorgente, invisibili nel browser su cui ti capita di testare, e invisibili alla tua suite di test. L'unico posto dove si vedono è il foglio di stile che il browser ha scaricato.
Questo è il pattern che spedisco in launch — demo live su launch.violettadev.com.