CSS設計

Tailwind CSSで@applyに頼らずコンポーネントを再利用する

読了 6分
CSS設計

Tailwind CSSで@applyに頼らずコンポーネントを再利用する

Tailwindを使っていると、同じユーティリティの羅列が何度も登場します。「これ、まとめたいな」と思って最初は @apply に手を伸ばしたのですが、あとで管理が面倒になったので、考え直したときのメモです。

結論から書くと、@apply でCSSクラスを作るより、UIコンポーネントとして切り出す ほうが見通しよく保てる場面がほとんどでした。理由と具体的なやり方を整理します。

@applyの何が問題だったか

@apply はユーティリティをまとめて独自クラスを作る機能です。一見すると便利なのですが、多用するとTailwindの利点を打ち消してしまうみたいです。

/* @apply でボタンクラスを作る例 */
.btn-primary {
  @apply inline-flex items-center rounded-lg bg-sky-600 px-4 py-2 text-sm font-semibold text-white;
}
.btn-primary:hover {
  @apply bg-sky-700;
}

これには次の難点がありました。

  • マークアップとスタイルが 別ファイルに分離 して、結局CSSとHTMLを行き来することになる(Tailwindが解こうとした問題に逆戻り)
  • クラス名を考える手間が戻ってくる(btn-primarybutton-primary か…)
  • バリエーションが増えるたびにCSS側が膨らむ

公式ドキュメントでも、@apply は控えめに使うことが推奨されています。やっぱりそうですよね、という感じです。

まずはコンポーネントに切り出す

ReactやVueを使っているなら、再利用の単位は コンポーネント にするのが素直です。スタイルはユーティリティのまま、まとめる対象をマークアップごとにします。

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>

export function Button({ className = '', ...props }: ButtonProps) {
  return (
    <button
      className={`inline-flex items-center rounded-lg bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-700 ${className}`}
      {...props}
    />
  )
}

こうすれば、使う側は <Button>送信</Button> と書くだけです。スタイルの実体は1か所に集まって、しかもマークアップと一緒に管理できます。className を受け取って末尾に連結しておくと、呼び出し側で部分的に上書きする余地も残せます。

バリエーションは「条件で切り替える」

色やサイズの違いは、propsで受けて条件分岐にします。文字列を連結するだけでも十分実用的です。

type Variant = 'primary' | 'secondary'

const base = 'inline-flex items-center rounded-lg px-4 py-2 text-sm font-semibold'

const variants: Record<Variant, string> = {
  primary: 'bg-sky-600 text-white hover:bg-sky-700',
  secondary: 'bg-stone-100 text-stone-700 hover:bg-stone-200',
}

export function Button({ variant = 'primary', className = '', ...props }: ButtonProps & { variant?: Variant }) {
  return <button className={`${base} ${variants[variant]} ${className}`} {...props} />
}

Record<Variant, string> で定義しておくと、バリアントを追加したときに定義漏れをコンパイラが教えてくれます。これは便利です。条件分岐が複雑になってきたら、clsxtailwind-variants といった小さなライブラリを足すと、可読性を保ったまま整理できます。

クラスの重複は変数にまとめる

コンポーネントにするまでもない短い繰り返しは、ただの定数に切り出すだけでも十分です。

const cardClass = 'rounded-2xl bg-white p-4 shadow-sm'

export function Cards() {
  return (
    <div className="grid gap-4 sm:grid-cols-2">
      <div className={cardClass}>...</div>
      <div className={cardClass}>...</div>
    </div>
  )
}

CSSを増やさずに重複を減らせるので、まずはこの手軽な方法から検討するとよさそうです。

それでも@applyが向く場面

とはいえ、@apply を完全に避ける必要はないようです。次のようなケースでは妥当だと思います。

  • コンポーネント化できない、素のHTMLやMarkdown由来の要素に基本スタイルを当てたいとき
  • フォーカスリングなど、プロジェクト全体で共通の「ベース層」を整えるとき
@layer base {
  /* MDXで生成される見出しなど、クラスを付けられない要素向け */
  .prose-content a {
    @apply text-sky-600 underline hover:text-sky-800;
  }
}

ポイントは「コンポーネントとして持てるものはコンポーネントに、持てないものだけ @apply」という線引きです。

まとめ

  • 再利用の第一候補は @apply ではなく コンポーネント化
  • バリエーションはpropsと条件分岐(必要なら clsx などを併用)
  • 短い重複はただの定数にまとめるだけでも効果的
  • クラスを付けられない要素のベース整形には @apply も有効

Tailwindの強みは「マークアップを見ればスタイルが分かる」ことだと思います。その利点を保てるかどうかを基準に、まとめ方を選ぶと迷いにくくなりました。詳しくはTailwind公式のスタイル再利用ガイドも参考になります。以上です。