filter と backdrop-filter では、Lightning CSS がプレフィックス付きとプレフィックスなしの宣言を1つにマージし、最後に書いたほうのプレフィックスフラグを残す。つまり backdrop-filter の後ろに -webkit-backdrop-filter を書くと、WebKit 専用にコンパイルされる。Chrome は -webkit-backdrop-filter をまったくサポートしていないので、ソースは正しく見えたまま、本番では blur が黙って消える。他のプレフィックス付きプロパティはたいてい同じことをしても無事なので、なおさら見落としやすい。
症状
自分のテンプレートの1つで、sticky ヘッダーにすりガラス効果をかけていた。半透明の背景と、その裏側の blur だ。手元では何週間も動いていた。ところがデプロイ済みのサイトを Chrome で見たら、ヘッダーはただの半透明な板になっていた。blur がない。その下をコンテンツがくっきりしたままスクロールしていく。
ソースはそうは言っていなかった。ルールは globals.css にちゃんとあるし、要素にクラスも付いている。メディアクエリで隠されてもいないし、@supports のゲートもない。DevTools でも要素がそのルールにマッチしていた。
最初に疑ったこと
CSS の詳細度の問題か、スタッキングコンテキストの問題だと思った。backdrop-filter は背後にあるものや、包含ブロックを作る祖先要素に敏感なことで有名だ。親要素の transform と filter を調べるのに、恥ずかしいくらい時間を使った。
これは間違いで、間違えた理由は ソースを読んでいて、ブラウザがダウンロードしたスタイルシートを読んでいなかったから だ。
実際の仕組み
テンプレートは Next.js 16.2.10(package.json でピン留め)でビルドしている。tailwindcss と @tailwindcss/postcss は ^4 と宣言していて、自分の環境では 4.3.2 に解決される。PostCSS の設定はこれだけだ:
// launch-template/postcss.config.mjs
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;Tailwind v4 は Lightning CSS を通して CSS をコンパイルする — 自分の環境では lightningcss@1.32.0 だ。Lightning CSS は -webkit-backdrop-filter と backdrop-filter を無関係な2つのプロパティとして扱わない。両方を、ベンダープレフィックスのフラグ集合を持つ1つの内部プロパティとしてパースし、生き残ったフラグだけを再出力する。
同じプロパティに宣言が2つあれば、カスケードの規定どおり後のほうが勝つ。ただし「勝つ」のは プレフィックスのフラグも含めた 宣言全体だ。標準プロパティを先に、WebKit 版を後に書けば、標準のフラグは消える。
確認のためにコンパイラを直接動かした。ビルドが使うのと同じ lightningcss@1.32.0 だ:
### 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); }最後の行をよく読んでほしい。標準プロパティ だけ を書けば、両方が出力される。Safari にはプレフィックスが必要だとコンパイラはすでに知っていて、自分で足してくれる。念のためにと足した2つ目の宣言は、安全を足すどころか標準プロパティを壊していた。
これは WebKit が優先されているのではなく、本当に「最後のフラグが勝つ」だけだ。ターゲットを何も設定しないと、潰れる向きが逆になる:
### no targets at all
standard, then -webkit- => .a { -webkit-backdrop-filter: blur(16px); }
-webkit-, then standard => .b { backdrop-filter: blur(16px); }やられるのは filter 系だけで、全プロパティではない
最初はコードベースから手書きのプレフィックスを全部剥がそうと思った。その前に、標準の次に webkit という同じ組み合わせをいろいろなプロパティでコンパイラに通してみたら、結果を見て手が止まった:
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 dropped標準プロパティを失うのは filter 系だけだ。それ以外は両方の形式を出力するか、標準のほうへ 寄って 潰れるので、そこでの冗長なプレフィックスは本当に無害だ。
2つのうちでは filter のほうがたちが悪いとも言える。このターゲット構成では filter: blur(4px) 単独は filter: blur(4px) にコンパイルされる — どのターゲットもプレフィックスを必要としない — だからその後ろに -webkit-filter を書くと、動いている宣言を、サポート対象のどのブラウザも求めていない宣言に置き換えることになる。
この一貫性のなさが本当の罠だ。user-select を確認して、出力に両方の形式があるのを見て、防御的なプレフィックスは問題ないと結論づけ、そうではない2つのプロパティを確認しようとは思いもしない。
なぜ Chrome だけ何も描画しないのか
Blink は WebKit 由来だから、Chrome も -webkit-backdrop-filter をレガシーな別名として受け付けるだろうと半ば思い込んでいた。受け付けない。Chrome 151 では:
CSS.supports('backdrop-filter', 'blur(16px)') // true
CSS.supports('-webkit-backdrop-filter', 'blur(16px)') // falseインラインで指定しても捨てられる。標準プロパティにマッピングされることすらない。そこで実際の要素で試してみた:
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" だ。コンパイラが出力した宣言は、Chrome が完全に無視するものだった。Safari では動き続けていた。だからこそ、これだけ長くレビューを通り抜けてしまった。
決着をつける診断
ソースを読むのをやめる。ブラウザが実際に受け取ったバイト列を読む:
# 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 -lこの2つの数が一致しなければ、宣言が潰れている。現在のビルドでは一致している — 11 と 11 だ。デプロイ済みのスタイルシートからそのまま持ってきた、実際に配信されている nav のルールがこれだ:
.lv-nav{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}そして、これを得るために自分が書いたのが、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);
}入力は1宣言、出力は2つ。オーバーレイのバリアントでは、transition のプロパティリストにもコンパイラがプレフィックスを付けてくれていた: transition:...,-webkit-backdrop-filter .4s,backdrop-filter .4s。
すでに壊れているページで因果関係を確かめたいなら、DevTools で逆をやればいい。要素を選んで Styles ペインに backdrop-filter: blur(16px) を手で足す。すぐに blur が出たら、コンパイラが標準プロパティを食べている。
再発をどう止めるか
手書きの backdrop-filter のペアを削除して、誘惑が生まれる場所にコメントを残した。5つあるテンプレートのうち4つの globals.css に入れてある(文面は同じで、各ファイルの幅に合わせて折り返した):
/* 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. */コメントはリマインダーであってガードではない。実際にこれを捕まえるのは、同じブロック内でプレフィックス付きとなしの両方で現れるプロパティをルール本体からスキャンするスクリプトだ — 今のところ CI には組み込まず、必要なときに手で走らせている。5つのテンプレート、933個の CSS/TSX/TS ファイル全体で、filter と backdrop-filter のペアはゼロ。噛みついてくるのはこの2つだ。
他のプロパティのペアはまだ見つかる。あるテンプレートには mask-image のペアが3つと background-clip のペアが2つ、別のテンプレートには user-select のペアが2つと appearance のペアが1つ。判断する前にそれぞれコンパイラに通して、そのまま残した。どれも両方の形式を出力する。冗長ではあるが危険ではない。残っている -webkit- 宣言は、そもそも標準の同等品が存在しないプロパティだ: -webkit-font-smoothing、::-webkit-scrollbar、-webkit-text-size-adjust、::-webkit-details-marker、-webkit-overflow-scrolling、-webkit-user-drag。
同じガードを作るなら、すべてのペアで落とすようにはしないこと。本当に危ない2つが、無害な山の下に埋もれてしまう。filter と backdrop-filter ではビルドを落とし、それ以外は警告にとどめる。
一般化したルール、そしてここだけは覚えておく価値がある部分: 現代の CSS パイプラインはプレフィックスを自動で付けてくれる。手書きのプレフィックスは冗長な保険ではなく、本物を上書きしうる余計な宣言だ。 Tailwind v4 に移行して、2019年製の防御的なプレフィックスだらけのスタイルシートを引き継いだなら、その大半はただのお荷物で済む。だが filter と backdrop-filter のものは生きたバグだ — しかもソースでは見えず、たまたまテストしているブラウザでも見えず、テストスイートにも見えない。姿を現す唯一の場所は、ブラウザがダウンロードしたスタイルシートだ。
これは launch で採用しているパターンだ — ライブデモは launch.violettadev.com。