clsx・tailwind-mergeにおけるフォント関連スタイルの競合問題と解決策

に公開

TailwindCSSを実務で活用しており、clsx・tailwind-mergeを導入している開発者の方であれば、スタイルが内部的に競合し、期待した結果と異なる表示になってしまう問題に遭遇したことがあるかもしれません。今回、フォント関連のスタイル競合によるバグに遭遇したため、備忘録として解決策を共有します。

バグの概要

今回遭遇した問題は、フォントスタイル(フォントファミリー、サイズ、太さ、行高など)をpropsとして受け取る見出しコンポーネントにおいて、tailwind-mergeに渡すクラスの順序によってCSSの適用結果が変わってしまうという現象です。

使用したコンポーネントについて

今回使用した見出しコンポーネントなどについては以下のコードをご確認ください。

src/components/head-title.tsx

interface titleProps {
  title: string;
  fontFamily: string;
  fontSize: string;
  fontWeight: string;
  LineHeight: string;
}

export default function HeadTitle({
  title,
  fontFamily,
  fontSize,
  fontWeight,
  LineHeight,
}: titleProps) {
  return (
    <>
      <div>
        <h1 className={cn([fontFamily, fontSize, fontWeight, LineHeight])}>
          {title}
        </h1>
        <h1 className={cn([fontSize, LineHeight, fontWeight, fontFamily])}>
          {title}
        </h1>
        <h1
          className={cn([fontSize, LineHeight, fontWeight])}
          style={{ fontFamily }}
        >
          {title}
        </h1>
      </div>
    </>
  );
}


<HeadTitle
  title="HeadingTitle"
  fontSize="text-3xl"
  fontWeight="font-[700]"
  LineHeight="leading-[1.5]"
  fontFamily="font-(family-name:--font-noto-sans-jp)"
/>

バグが発生する条件

早速どういう状況でバグが発生するか場合分けで見ていこうと思います。

パターン① fontFamily → fontSize → fontWeight → LineHeightの順序

結果: uni-sans-serif(デフォルトフォント)、行高1.5、太さ700

  • フォントファミリーが適用されず、デフォルトフォントが使用される
  • フォントウェイトは正常に適用される

パターン② fontSize → LineHeight → fontWeight → fontFamilyの順序

結果: Noto Sans JP、行高1.5、太さ400(デフォルト)

  • フォントファミリーは正常に適用される
  • フォントウェイトがデフォルト値に戻ってしまう

原因の分析

この現象は、おそらくtailwind-mergeの競合による問題であると考えられます。

  1. クラスのグループ化: tailwind-mergeはfont-*系のクラスを同一グループとして扱う
  2. 競合解決: 同じプロパティに影響するクラス同士で競合が発生した場合、配列内で後に位置するクラスが優先される
  3. CSS変数の扱い: font-(family-name:--font-noto-sans-jp)のようなCSS変数を使用したクラスと、通常のfont-boldなどのクラスが競合する

バグの回避アプローチ

フォントファミリーのみをインラインスタイルで指定し、その他のフォント関連クラスをtailwind-mergeで処理する方法が個人的なベストプラクティスであると考えています。

<h1 className={cn([fontSize, LineHeight, fontWeight])}
	style={{ fontFamily }}>				
{title}
</h1>

このアプローチの利点と欠点

利点

  • 確実性: インラインスタイルは最も高い特異性を持つため、競合を回避できる
  • 部分的な活用: tailwind-mergeの恩恵を他のスタイルでは継続して受けられる

欠点

  • 一貫性の欠如: フォントファミリーのみ異なる指定方法になる
  • 型安全性: CSS変数を文字列として渡すため、TypeScriptでの型チェックが効きにくい

まとめ

tailwind-mergeは非常に便利なライブラリですが、CSS変数を使用したクラスと通常のTailwindクラスとの間で予期しない競合が発生する場合があります。このような問題に遭遇した際は、インラインスタイルを部分的に活用することで確実に回避できます。

Discussion