任意バリアント `[&:has(…)]` を `has-[…]` 形式へ自動正規化
Tailwind CSS v4 のキャノニカライゼーション機能が拡張され、任意バリアント形式の [&:has(…)] が組み込みの has-[…] バリアントへ自動変換されるようになりました。さらに、has-[…] の内側のセレクターが既知のバリアントと対応する場合は、そこまで踏み込んだ変換も行われます。
背景
この変更は、IntelliSense の誤検知として報告された tailwindlabs/tailwindcss-intellisense#1562 を調査する中で着目された問題に対処しています。Tailwind CSS v4 では、has-[…] という組み込みバリアントが存在するにもかかわらず、同等の意味を持つ [&:has(…)] 形式の任意バリアントをそのまま書けてしまうという非一貫性がありました。
[&:has(…)] は「現在の要素(&)が :has() セレクターを満たす場合」を表す任意バリアントであり、has-[…] と意味的に等価です。しかし、キャノニカライゼーションがこの変換を行わなかったため、同一のスタイルを異なる記法で表現できる状態が残っていました。これが IntelliSense での誤検知の遠因にもなっていたと考えられます。
技術的な変更
canonicalize-candidates.ts の modernizeArbitraryValuesVariant 関数に、[&:has(…)] を検出して has-[…] へ置換するロジックが追加されました。
変換の対象となる条件は次のとおりです:
-
トップレベルのバリアントであること(
group-[&:has(…)]のようにネストされている場合は対象外) - AST が
&セレクターと:has()関数の2ノード構成であること :has()の引数が単一のセレクターノードであること
これらの条件を満たす場合、SelectorParser.toCss() で :has() 内部のセレクターをCSS文字列化し、designSystem.parseVariant() へ has-[…] 形式として渡すことで、既存の変換パイプラインを再利用します。
// `[&:has(…)]` can be replaced with `has-[…]`
if (
// Only top-level, so `group-[&:has(…)]` is not covered
parent === null &&
// [&:has(…)]:flex
// ^ ^^^^^^
ast.length === 2 &&
ast[0].kind === 'selector' &&
ast[0].value === '&' &&
ast[1].kind === 'function' &&
ast[1].value === ':has' &&
ast[1].nodes.length === 1 &&
ast[1].nodes[0].kind === 'selector'
) {
replaceObject(
variant,
designSystem.parseVariant(`has-[${SelectorParser.toCss(ast[1].nodes)}]`),
)
continue
}
変換は has-[…] への置換で終わらず、その後の既存パイプラインによってさらなる正規化が連鎖します。たとえば data-* 属性セレクターは has-data-[…] へ、aria-*="true" 形式は対応する組み込みバリアントへとそれぞれ変換されます:
| 変換前 | 変換後 |
|---|---|
[&:has([role=checkbox])]:flex |
has-[[role=checkbox]]:flex |
[&:has([aria-visible="true"])]:flex |
has-aria-visible:flex |
[&:has([data-slot=description])]:flex |
has-data-[slot=description]:flex |
設計判断
group-[&:has(…)] などのネスト形式を対象外とした点が注目されます。条件に parent === null(トップレベル限定)を設けているのは、ネストされた文脈では意味論が変わりうるためです。スコープを限定することで、変換の安全性を優先しています。
また、has-[…] への初期変換後は parseVariant() を経由することで、既存の data-* や aria-* の正規化ロジックを新たに実装することなく再利用できる設計になっています。変換パイプラインを多段に連鎖させることで、コードの追加量を最小限(22行)に抑えながら、複数のケースに対応しています。
まとめ
本PRにより、[&:has(…)] という冗長な任意バリアント記法は、より簡潔で意味が明確な has-[…] 系バリアントへ自動的に正規化されます。既存の変換パイプラインを再利用する設計により、data-* や aria-* の最適化も追加コストなく恩恵を受けられる点は、Tailwind CSS のキャノニカライゼーション基盤の成熟を示しています。