Para filter e backdrop-filter, o Lightning CSS funde a declaração com prefixo e a declaração sem prefixo numa só, mantendo as flags de prefixo daquela que escreveste por último. Ou seja, backdrop-filter seguido de -webkit-backdrop-filter compila para WebKit apenas. O Chrome não suporta de todo -webkit-backdrop-filter, por isso o blur desaparece silenciosamente em produção enquanto o código-fonte parece correto. A maioria das outras propriedades com prefixo sobrevive ao mesmo tratamento sem dano — que é precisamente o que torna esta fácil de deixar passar.
O sintoma
Um header sticky de um dos meus templates tinha um efeito de vidro fosco: fundo translúcido, blur por trás. Funcionou na minha máquina durante semanas. Depois fui ver o site publicado no Chrome e o header era só translúcido, liso. Sem blur. O conteúdo passava por baixo perfeitamente nítido.
O código-fonte dizia o contrário. A regra estava mesmo ali no globals.css, a classe estava no elemento, nenhuma media query a escondia e não havia nenhum gate de @supports. As DevTools mostravam que o elemento correspondia à regra.
O que assumi
Assumi um problema de especificidade CSS, ou um problema de stacking context — backdrop-filter é famosamente sensível ao que está por trás dele e a ancestrais que criam containing blocks. Gastei uma quantidade embaraçosa de tempo com transform e filter em elementos pai.
Estava enganado, e a razão para estar enganado é que eu estava a ler o código-fonte, não a folha de estilos que o browser descarregou.
O mecanismo real
Os templates fazem build com Next.js 16.2.10 (fixado no package.json); tailwindcss e @tailwindcss/postcss estão declarados como ^4 e resolvem para 4.3.2 na minha instalação. A configuração de PostCSS é só isto:
// launch-template/postcss.config.mjs
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;O Tailwind v4 compila o CSS através do Lightning CSS — lightningcss@1.32.0 na minha instalação. O Lightning CSS não trata -webkit-backdrop-filter e backdrop-filter como duas propriedades sem relação. Faz o parse de ambas para uma única propriedade interna que transporta um conjunto de flags de prefixo de fabricante, e depois volta a emitir as flags que sobreviverem.
Duas declarações para a mesma propriedade significa que ganha a última, como a cascata exige. Mas o que "ganha" é a declaração inteira incluindo as suas flags de prefixo. Escreve primeiro a propriedade padrão e a de WebKit a seguir, e a flag padrão desaparece.
Corri o compilador diretamente para confirmar. O mesmo lightningcss@1.32.0 que o build usa:
### 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); }Lê essa última linha com atenção. Escrever apenas a propriedade padrão produz as duas. O compilador já sabe que o Safari precisava do prefixo e acrescenta-o. A minha segunda declaração "por precaução" não acrescentou segurança — destruiu a propriedade padrão.
É mesmo ganha-a-última-flag, não uma preferência por WebKit. Sem quaisquer targets configurados, o colapso corre no sentido contrário:
### no targets at all
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { backdrop-filter: blur(16px); }É a família filter, não todas as propriedades
O meu primeiro instinto foi arrancar do codebase todos os prefixos escritos à mão. Antes de o fazer, passei o mesmo par padrão-depois-webkit pelo compilador para um leque de propriedades, e o resultado travou-me:
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 droppedSó a família filter perde a propriedade padrão. Tudo o resto ou emite as duas formas ou colapsa na direção da padrão, por isso um prefixo redundante aí é genuinamente inofensivo.
filter é provavelmente a pior das duas. Com estes targets, filter: blur(4px) sozinho compila para filter: blur(4px) — nenhum target precisa de prefixo — por isso escrever -webkit-filter a seguir substitui uma declaração que funciona por outra que nenhum browser da tua matriz de suporte pediu.
Esta inconsistência é a verdadeira armadilha. Verificas user-select, vês as duas formas no output, concluis que os teus prefixos defensivos estão bem, e nunca te lembras de verificar as duas propriedades onde não estão.
Porque é que o Chrome em particular não renderiza nada
Tinha meio que assumido que o Chrome aceitaria -webkit-backdrop-filter como alias legacy, já que o Blink deriva do WebKit. Não aceita. No Chrome 151:
CSS.supports('backdrop-filter', 'blur(16px)') // true
CSS.supports('-webkit-backdrop-filter', 'blur(16px)') // falseDefini-lo inline é ignorado por completo — nem sequer mapeia para a propriedade padrão. Por isso testei-o num elemento real:
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". A declaração que o compilador emitiu é uma que o Chrome ignora completamente. O Safari continuou a funcionar, que é precisamente porque isto sobreviveu à revisão durante tanto tempo.
O diagnóstico que resolve a questão
Para de ler o teu código-fonte. Lê os bytes que o browser recebeu mesmo:
# 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 esses dois números não baterem certo, tens declarações colapsadas. No build atual batem — 11 e 11. Esta é a regra de nav que foi para produção, tal e qual como está na folha de estilos publicada:
.lv-nav{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}E isto é o que eu escrevi para a obter, em 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);
}Uma declaração à entrada, duas à saída. O compilador até me pôs o prefixo na lista de propriedades do transition na variante overlay: transition:...,-webkit-backdrop-filter .4s,backdrop-filter .4s.
Se quiseres confirmar a causa numa página que já está partida, faz o inverso nas DevTools: seleciona o elemento e acrescenta backdrop-filter: blur(16px) à mão no painel Styles. Se o blur aparecer instantaneamente, o compilador comeu a tua propriedade padrão.
Como evito que volte a acontecer
Removi os pares de backdrop-filter escritos à mão e deixei um comentário onde mora a tentação, no globals.css de quatro dos cinco templates (texto idêntico, ajustado à largura de cada ficheiro):
/* 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. */Um comentário é um lembrete, não um guard. O que apanha isto a sério é um script que percorre os corpos das regras à procura de qualquer propriedade que apareça com e sem prefixo no mesmo bloco — para já corrido a pedido, em vez de ligado ao CI. Nos cinco templates, 933 ficheiros CSS/TSX/TS, não encontra nenhum par de filter ou backdrop-filter. Esses é que mordem.
Encontra à mesma pares noutras propriedades: três pares de mask-image e dois de background-clip num template, dois pares de user-select e um par de appearance noutro. Passei cada um pelo compilador antes de decidir, e deixei-os em paz — todos emitem as duas formas. Redundantes, não perigosos. As declarações -webkit- que restam são propriedades sem qualquer equivalente padrão: -webkit-font-smoothing, ::-webkit-scrollbar, -webkit-text-size-adjust, ::-webkit-details-marker, -webkit-overflow-scrolling, -webkit-user-drag.
Se construíres o mesmo guard, não o faças falhar em todos os pares — vais enterrar os dois casos reais debaixo de uma pilha de inofensivos. Faz falhar o build em filter e backdrop-filter; nos restantes, avisa apenas.
A regra geral, e a parte que vale a pena guardar: os pipelines modernos de CSS põem os prefixos por ti, e um prefixo escrito à mão não é um seguro redundante — é uma declaração extra que pode sobrepor-se à verdadeira. Se migraste para Tailwind v4 e herdaste uma folha de estilos cheia de prefixos defensivos de 2019, a maioria é apenas peso morto, mas os de filter e backdrop-filter são bugs vivos — e são invisíveis no código-fonte, invisíveis no browser em que calha estares a testar, e invisíveis para a tua suite de testes. O único sítio onde aparecem é a folha de estilos que o browser descarregou.
Este é o padrão que envio no launch — demo ao vivo em launch.violettadev.com.