`@variant`でコンパウンド・スタックバリアントが使えるように

tailwindlabs/tailwindcss

@variantディレクティブがコンマ区切りのコンパウンドバリアントとコロン区切りのスタックバリアントをサポートし、複数バリアントの指定を簡潔に記述できるようになりました。

背景

@variantディレクティブは当初、シンプルさを優先して単一バリアントのみをサポートする設計で導入されました。複数のバリアントに同じスタイルを適用したい場合、開発者はルールの重複記述や@variantのネストを強いられていました。たとえばhoverfocusの両方に同じスタイルを適用するには、同一ルールを2回書くか、ネストを深くする必要がありました。

コミュニティからの要望(#19526#19884)を受け、機能の必要性が確認できたタイミングで本PR(#19996)がこれらを統合・発展させる形で実装されました。

技術的な変更

packages/tailwindcss/src/variants.tssubstituteAtVariant 関数が拡張され、バリアントパラメータのコンマ区切り・コロン区切りをパースして展開する処理が追加されました。

変更前:

// Starting with the `&` rule node
let node = styleRule('&', variantNode.nodes)

let variant = variantNode.params

let variantAst = designSystem.parseVariant(variant)
if (variantAst === null) {
  throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`)
}

let result = applyVariant(node, variantAst, designSystem.variants)
if (result === null) {
  throw new Error(`Cannot use \`@variant\` with variant: ${variant}`)
}

変更後:

let nodes: AstNode[] = []
let compoundVariants = segment(variantNode.params, ',')
for (let [idx, compoundVariant] of compoundVariants.entries()) {
  let node = styleRule(
    '&',
    idx === compoundVariants.length - 1
      ? variantNode.nodes
      : variantNode.nodes.map(cloneAstNode),
  )

  let stackedVariants = segment(compoundVariant, ':')
  for (let i = stackedVariants.length - 1; i >= 0; --i) {
    let variant = stackedVariants[i].trim()

    let variantAst = designSystem.parseVariant(variant)
    if (variantAst === null) {
      throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`)
    }

    let result = applyVariant(node, variantAst, designSystem.variants)
    if (result === null) {
      throw new Error(`Cannot use \`@variant\` with variant: ${variant}`)
    }
  }

  nodes.push(node)
}

コンマ区切りのコンパウンドバリアントはそれぞれ独立したルールセットに展開されます。最後のコンパウンドバリアント以外では cloneAstNode を使ってノードを複製することで、各ルールが独立したAST表現を持つよう設計されています。これはソースマップのdst(出力先)位置情報を正確に記録するためにも必要な処理です。

コロン区切りのスタックバリアントは、右から左へ(内側から外側へ)ループしながら applyVariant を順に適用することで、ネスト構造を内部的に再現します。この処理順はTailwind CSSのクラス記法a:b:flexの解釈と一致します。

設計判断

「概念的な展開(conceptual expansion)」としてのセマンティクス が本機能の中心的な設計思想です。新構文は既存の冗長な記述に内部展開される糖衣構文として定義されており、動作の予測可能性を維持しています。

PRでは、コンパウンドバリアントとスタックバリアントを組み合わせた場合にCSS出力が大きく膨張する可能性が明示的に言及されています。@variant a, b:c { @variant d, e:f { … } } のような組み合わせは、展開後に多数のルールセットが生成されます。この設計は利便性と引き換えに出力サイズのトレードオフを伴うため、利用者が意識的に管理する必要があります。

後方互換性は完全に維持されており、既存の手動ネスト構文は引き続き動作します。新旧の記法を混在させることも可能です。

まとめ

@variantのコンマ区切りとコロン区切りのサポートにより、これまで冗長なネストや重複記述が必要だった複数バリアントの適用が、Tailwind CSSのクラス記法と統一されたシンタックスで簡潔に書けるようになりました。既存コードとの完全な後方互換性を保ちながら、CSSの可読性と保守性を高める実用的な改善です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
a6f10f93

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)の3部構成が明確に守られており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト、PR番号のリンク記法共に正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門知識を持つエンジニアを対象としており、前提知識の説明を省略し、本質的な技術的変更に焦点を当てている点が適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

Diffの内容を正確に反映しています。`packages/tailwindcss/src/variants.ts`の変更箇所を的確に抜き出し、解説しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「コンパウンドバリアント」「スタックバリアント」「AST」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コンマ区切りとコロン区切りの処理ロジック、`cloneAstNode`の役割など、技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(機能追加の背景、設計思想、トレードオフなど)がPR Descriptionの内容によって裏付けられており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#19996, #19526, #19884)がすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの主題である「`@variant`の複合・スタック対応」を的確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれない外部知識(バージョン情報、リリース予定など)の追記はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「当初は〜だった」「〜を強いられていた」など、変更前後の状況を示す時間表現がPRの内容と一致しており、正確です。