Para filter y backdrop-filter, Lightning CSS fusiona la declaración con prefijo y la que no lo tiene en una sola, y conserva las banderas de prefijo de la que escribiste al final. Así que backdrop-filter seguido de -webkit-backdrop-filter compila a solo-WebKit. Chrome no soporta -webkit-backdrop-filter en absoluto, así que el blur desaparece en silencio en producción mientras el código fuente se ve correcto. La mayoría de las demás propiedades con prefijo sobreviven al mismo tratamiento sin daño — que es justamente lo que hace que esta sea fácil de pasar por alto.
El síntoma
Un header sticky de una de mis plantillas tenía un efecto de vidrio esmerilado: fondo translúcido, blur por detrás. Funcionó en mi máquina durante semanas. Después miré el sitio desplegado en Chrome y el header era solo translúcido plano. Sin blur. El contenido pasaba por debajo perfectamente nítido.
El código fuente decía otra cosa. La regla estaba ahí mismo en globals.css, la clase estaba en el elemento, ninguna media query la ocultaba y no había ningún gate de @supports. DevTools mostraba que el elemento hacía match con la regla.
Lo que asumí
Asumí un problema de especificidad CSS, o un problema de stacking context — backdrop-filter es famosamente sensible a lo que tiene detrás y a los ancestros que crean containing blocks. Perdí una cantidad vergonzosa de tiempo con transform y filter en elementos padre.
Estaba equivocado, y la razón por la que estaba equivocado es que estaba leyendo el código fuente, no la hoja de estilos que descargó el navegador.
El mecanismo real
Las plantillas se compilan con Next.js 16.2.10 (pinneado en package.json); tailwindcss y @tailwindcss/postcss están declarados como ^4 y resuelven a 4.3.2 en mi instalación. La configuración de PostCSS es solo esto:
// launch-template/postcss.config.mjs
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;Tailwind v4 compila el CSS a través de Lightning CSS — lightningcss@1.32.0 en mi instalación. Lightning CSS no trata -webkit-backdrop-filter y backdrop-filter como dos propiedades sin relación. Parsea ambas a una sola propiedad interna que lleva un conjunto de banderas de prefijo de proveedor, y después vuelve a emitir las banderas que sobrevivan.
Dos declaraciones para la misma propiedad significa que gana la última, como exige la cascada. Pero lo que "gana" es la declaración completa incluyendo sus banderas de prefijo. Escribe primero la propiedad estándar y la de WebKit después, y la bandera estándar desaparece.
Corrí el compilador directamente para confirmarlo. El mismo lightningcss@1.32.0 que usa el 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); }Lee esa última línea con calma. Escribir solo la propiedad estándar produce las dos. El compilador ya sabe que Safari necesitaba el prefijo y lo agrega. Mi segunda declaración "por si acaso" no agregaba seguridad — destruía la propiedad estándar.
De verdad es gana-la-última-bandera, no una preferencia por WebKit. Sin ningún target configurado, el colapso corre en la dirección contraria:
### no targets at all
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { backdrop-filter: blur(16px); }Es la familia filter, no toda propiedad
Mi primer instinto fue arrancar del codebase todos los prefijos escritos a mano. Antes de hacerlo pasé el mismo par estándar-luego-webkit por el compilador para un abanico de propiedades, y el resultado me frenó:
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 familia filter pierde la propiedad estándar. Todo lo demás o emite ambas formas o colapsa hacia la estándar, así que un prefijo redundante ahí es genuinamente inofensivo.
filter es probablemente la peor de las dos. Con estos targets, filter: blur(4px) por sí sola compila a filter: blur(4px) — ningún target necesita prefijo — así que escribir -webkit-filter después reemplaza una declaración que funciona por una que ningún navegador de tu matriz de soporte pidió.
Esa inconsistencia es la verdadera trampa. Revisas user-select, ves ambas formas en la salida, concluyes que tus prefijos defensivos están bien, y nunca se te ocurre revisar las dos propiedades donde no lo están.
Por qué Chrome en particular no renderiza nada
Yo medio asumía que Chrome aceptaría -webkit-backdrop-filter como alias legacy, dado que Blink deriva de WebKit. No lo hace. En Chrome 151:
CSS.supports('backdrop-filter', 'blur(16px)') // true
CSS.supports('-webkit-backdrop-filter', 'blur(16px)') // falsePonerlo inline se descarta sin más — ni siquiera mapea a la propiedad estándar. Así que lo probé sobre un 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". La declaración que emitió el compilador es una que Chrome ignora por completo. Safari seguía funcionando, que es exactamente por qué sobrevivió tanto tiempo a la revisión.
El diagnóstico que lo zanja
Deja de leer tu código fuente. Lee los bytes que realmente recibió el navegador:
# 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 esos dos números no coinciden, tienes declaraciones colapsadas. En el build actual sí coinciden — 11 y 11. Esta es la regla de nav que se publicó, textual desde la hoja de estilos desplegada:
.lv-nav{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}Y esto es lo que escribí para conseguirla, en 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 declaración entra, dos salen. El compilador incluso me puso el prefijo en la lista de propiedades de transition en la variante overlay: transition:...,-webkit-backdrop-filter .4s,backdrop-filter .4s.
Si quieres confirmar la causa en una página que ya está rota, haz lo inverso en DevTools: selecciona el elemento y agrega backdrop-filter: blur(16px) a mano en el panel Styles. Si el blur aparece al instante, el compilador se comió tu propiedad estándar.
Cómo evito que vuelva a pasar
Quité los pares de backdrop-filter escritos a mano y dejé un comentario donde vive la tentación, en el globals.css de cuatro de las cinco plantillas (el texto es idéntico, ajustado al ancho de cada archivo):
/* 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 comentario es un recordatorio, no un guard. Lo que de verdad atrapa esto es un script que escanea los cuerpos de las reglas buscando cualquier propiedad que aparezca con y sin prefijo en el mismo bloque — por ahora se corre a demanda en vez de estar enganchado a CI. En las cinco plantillas, 933 archivos CSS/TSX/TS, encuentra cero pares de filter o backdrop-filter. Esos son los que muerden.
Sí encuentra pares de otras propiedades: tres pares de mask-image y dos de background-clip en una plantilla, dos pares de user-select y un par de appearance en otra. Pasé cada uno por el compilador antes de decidir, y los dejé como estaban — todos emiten ambas formas. Redundantes, no peligrosos. Las declaraciones -webkit- que quedan son propiedades sin ningún equivalente estándar: -webkit-font-smoothing, ::-webkit-scrollbar, -webkit-text-size-adjust, ::-webkit-details-marker, -webkit-overflow-scrolling, -webkit-user-drag.
Si construyes el mismo guard, no lo hagas fallar con cada par — vas a enterrar los dos casos reales bajo una pila de inofensivos. Haz fallar el build con filter y backdrop-filter; para el resto, solo advierte.
La regla general, y la parte que vale la pena guardar: los pipelines modernos de CSS ponen los prefijos por ti, y un prefijo escrito a mano no es un seguro redundante — es una declaración extra que puede sobrescribir la verdadera. Si migraste a Tailwind v4 y heredaste una hoja de estilos llena de prefijos defensivos de 2019, la mayoría son solo peso muerto, pero los de filter y backdrop-filter son bugs vivos — y son invisibles en el código fuente, invisibles en el navegador que te toca estar probando, e invisibles para tu suite de tests. El único lugar donde aparecen es la hoja de estilos que descargó el navegador.
Este es el patrón que envío en launch — demo en vivo en launch.violettadev.com.