ユーティリティーファーストなCSSフレームワーク「Tailwind CSS」のメリットとデメリットの克服方法フルスタックフレームワーク、T3 Stack入門(4)

フロントエンドエンジニアに向けて、Webアプリケーション開発のためのフルスタックフレームワークT3 Stackを解説する本連載。第4回はユーティリティーファーストのCSSフレームワーク「Tailwind CSS」について解説する。

» 2024年07月31日 05時00分 公開
[上原潤二NTTテクノクロス]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 NTTテクノクロス上原です。連載「フルスタックフレームワーク、T3 Stack入門」も、はや第4回を迎えました。「T3 Stack」のプロジェクトの初期構築ツール、「Create T3 App」もバージョンアップが続けられ、前回紹介したDrizzle対応に加え、7.32.0ではついにNext JSのApp Routerが実験段階(Experimental)を卒業し、正式機能としてデフォルト(初期設定)の選択肢となりました。

 おさらいになりますが、T3 Stackは、以下を構成要素として疎結合で結び付けたフルスタックフレームワークです。

T3 Stackの構成要素 説明 連載掲載
Next.JS Reactベースのフルスタック(サーバ+フロント)フレームワーク  
tRPC TypeScriptに基づいたRPCライブラリ 連載第2回
PrismaDrizzle ORM TypeScriptと連動するORM(サーバサイド) 連載第3回
Tailwind CSS ユーティリティーファーストのCSSフレームワーク 連載第4回(今回)
Auth.js OAuthなど利用可能な認証ライブラリ。旧名NextAuth.js  
T3 Env 型安全な環境変数設定を読み込むためのライブラリ  

 今回は、主にTailwind CSSについて紹介します。次回は、チュートリアルの中で、App RouterでtRPCを活用するコード例を解説し、「Server Actions」との比較などを紹介します。

Tailwind CSSの概要

 Tailwind CSSは、CSSの記述方法であり、ツールチェインであり、手法や考え方も含むWebページ、Webアプリケーションのスタイル設定のソリューションです。Tailwind CSSは素のHTMLにも適用できますが、Reactをはじめとするコンポーネント指向のユーザーインタフェース(UI)ライブラリで活用することで真価が発揮されます。

Tailwind CSSの使用例

 まずは例を見てみましょう。以下はTailwind CSSのクラス名でマークアップしたReactコンポーネントの例です。次回解説するチュートリアルで使用するコードから抜粋しています。

const TodoApp: NextPage = async () => {
  return (
    <main className="min-h-screen bg-slate-950 p-2">              (1)
      <div className="container mx-auto flex flex-col p-4">
        <h1 className="border-2 border-amber-500 p-3 text-center text-4xl font-bold text-amber-500 shadow-neon">
          Todoアプリ
        </h1>
        <br />
        <TodoList />
      </div>
    </main>
  );
};
export default TodoApp;

 ここでのJSXのclassName属性に指定しているクラス名、例えば(1)の「min-h-screen bg-slate-950 p-2」はそれぞれ以下のような意味になります。

Tailwind CSSが提供するユーティリティークラス名 CSSでの表記
min-h-screen .min-h-screen {
min-height: 100vh;
}
bg-slate-950 .bg-slate-950 {
--tw-bg-opacity: 1;
background-color: rgb(2 6 23 / var(--tw-bg-opacity));
}
p-2 .p-2 {
padding: 0.5rem;
}

 これらのTailwind CSSのクラス名は「ユーティリティークラス」と呼びますが、CSSにおけるセレクタと値の両方を表現しています。Tailwind CSSのクラス名は「セレクタ+値」である名前がオブジェクトのアイデンティティーとなっているという意味で、アトミックであり即値的です。また、ユーティリティークラスは多くの場合単機能であり、1つのクラス名が1つの設定だけを表現します。従って、複雑なスタイルを指定するためには場合によっては多くの複数クラス名を指定することになります。

Tailwind CSSの利点

 Tailwind CSSのユーティリティークラス方式の利点のうち、記述に関わる主なものを以下に示します。

利点1:クラスセレクタの名称を考えたり工夫したりする必要がない

 ユーティリティークラスのクラス名は、「何に対してどんな値を設定するか」を直接的に表しています。例えば「p-2」であればパディング(要素内余白)が0.5rem(16px)であるという指定です。あくまでスタイル設定を具体的に表していて、例えば「title」や「btn」などの、アプリケーションのUIの中での「意味」を表す名称ではありません。

 従って、Tailwind CSSでは、都度クラスセレクタの名前を考えて命名するのではなく、指定したい内容によって自動的に決まる(既存のものから選ぶ)ので、クラス名を考える必要がありません。一連のスタイルをまとめて名前を付けることが必要なら、基本的にはCSSのレイヤーではなく、Reactコンポーネント定義やTypeScriptのレイヤーで行います。

利点2:コロケーション

 Tailwind CSSではクラス名と値をまとめて属性に記述してしまうことで、スタイル指定とコードが分離されず、記述量も減ります。編集時にCSSとJSXコードの頻繁に行き来をする必要がなくなり、編集作業が楽にかつ高速になります。この方法には慣れる必要があり学習コストはかかりますが、習熟すればこの利点は非常に大きいものです。

 一見するとstyle属性のインラインスタイルで指定するのと同じに見えるかもしれませんが、内部的にはCSSクラスが生成されるので疑似クラスの指定なども問題なくでき、スタイルシートと同じ範囲のことができます。

 また、従来のCSSやCSS Modulesでは、JSXとスタイル指定を結び付けるためにクラス名をいちいち考案する必要がありますが、Tailwind CSS方式ではそれを考えなくてもよくなることも大きな利点であり、高速開発と一貫性の維持に役立ちます。

利点3: デザインの統一のための制約の仕組みを持っている

 Tailwind CSSでは、スタイルの指定ではカラーパレットや余白のサイズなどで離散的な値のみを使用します。例えば、Tailwind CSSではデフォルト状態で以下の値のマージンを指定できます。

Tailwind CSSユーティリティークラス CSS Px数
.m-0 margin: 0 0px
.m-1 margin: 0.25rem; 4px
.m-2 margin: 0.5rem; 8px
.m-3 margin: 0.75rem; 12px
.m-4 margin: 1rem; 16px
.m-5 margin: 1.25rem; 20px
.m-6 margin: 1.5rem; 24px
.m-7 margin: 1.75rem; 28px
.m-8 margin: 2rem; 32px
.m-9 margin: 2.25rem; 36px
.m-10 margin: 2.5rem; 40px
.m-11 margin: 2.75rem; 44px
.m-12 margin: 3rem; 48px
.m-14 margin: 3.5rem; 56px
.m-16 margin: 4rem; 64px
.m-20 margin: 5rem; 80px
.m-24 margin: 6rem; 96px
.m-28 margin: 7rem; 112px
.m-32 margin: 8rem; 128px
.m-36 margin: 9rem; 144px
.m-40 margin: 10rem; 160px
.m-44 margin: 11rem; 176px
.m-48 margin: 12rem; 192px
.m-52 margin: 13rem; 208px
.m-56 margin: 14rem; 224px
.m-60 margin: 15rem; 240px
.m-64 margin: 16rem; 256px
.m-72 margin: 18rem; 288px
.m-80 margin: 20rem; 320px
.m-96 margin: 24rem; 384px

 Tailwind CSSではマージをピクセル単位では指定できません(※)し、かつ、m-13やm-15といった中間値のクラス名は意図的に用意されていません。

(※)Tailwind CSS 2.1以降で採用されたジャストインタイムモードでは"w-[762px]"といったブラケットを用いた記法で、任意の値やほぼ任意のCSS記述が指定できます。ただそれは脱出ハッチ的に考え常用はしないようにします。

 このことによって、アプリケーション全体で、ある場所では単位をpixelで指定し、ある場所ではremで指定しているというような不統一が生じることもないし、m-21がよいのかm-22がよいのかなど、どちらでも大差ない値で迷うことなく統一することができます。CSSをそのまま使うと全てが自由になってしまいますが、ユーティリティークラスを通じることで統一的に制約を加えられます。このようなスペーシングだけでなく、色名、タイポグラフィ、影付けなどについて同様に離散値による制約が加えられています。

 このことは、Tailwind CSS公式サイトでは「Tailwind CSSはデザインシステム構築のためのAPIである」と表現されています。

Tailwind CSSのデメリットとその克服

 しかしながら、Tailwind CSSには、従来のスタイルの指定方法と大きく異なり、比べるとデメリットにも感じられる部分もあるでしょう。幾つか見ていきます。

デメリット1:ユーティリティークラス名が覚えられない

 Tailwind CSSではたくさんのユーティリティークラス名を使用するので慣れるまでが大変です。頻繁に使うものはすぐに覚えられると思いますが、めったに使わないクラスについては数多くあるチートシートを活用して確認するのがお勧めです。

 また、コードを読むときにはマウスカーソルをホバーすることで対応するCSSの記述を表示できるIDE拡張を入れておくのが個人的には必須だと思っています。以下は「Visual Studio Code」拡張の「Tailwind CSS Intellisense」でカーソルをユーティリティークラスにホバーしたときに表示されるツールチップの例です。

デメリット2:クラス名が長くなると可読性が悪化する

 Tailwindでは以下のようにクラス名指定が長くなることもあります。

<button
  type="submit"
   className="mb-4 rounded-full border-8 border-blue-400 px-4 py-2 font-bold
   text-blue-500 transition duration-500 ease-in-out hover:border-blue-600
   hover:bg-slate-300 hover:text-blue-900 focus:shadow-xl"
>Tailwind Button</button>

 ここで定義しているのは以下のようなホバーやフォーカスのバリエーション付きのスタイル指定をしたボタンです。

 ある程度以上にクラス名指定が長くなると、クラス指定が同じかどうかを目視で確認するのも難しくなってきます。

 この問題については、クラス名の文字列を名前の付いた変数としてグルーピングし、結合することが解決策の一つになります。例えば、classnamesや、clsxなどのクラス名結合ライブラリを用いて以下のようにすることができます。

import clsx from "clsx";
 :
  const color = `text-blue-500 hover:border-blue-600 hover:bg-slate-300
 hover:text-blue-900`;
  const spacing = `mb-4 px-4 py-2 border-8`;                                 (1)
  const decoration = `font-bold rounded-full focus:shadow-xl`;
  const animation = `transition duration-500 ease-in-out `;
 :
      <button
        className={clsx([color, spacing, textDecoration, animation, "p-8" ])}   (2)
      >
        Tailwind Button
      </button>
clsxを使った例

 ただし、単純な文字列結合だと、Tailwind CSSが提供するクラス名間で矛盾するものがあったとき、意図しない結果になる可能性があります。上の例では(1)px-4 py-2の指定があるので、(2)p-8の効果が出ません。

 この問題は、Tailwind CSSのユーティリティークラスとしての意味を考慮したクラス名を構築するTailwind Mergeといったライブラリを使用することで解決できます。

import { twMerge } from "tailwind-merge";
      <button
        className={twMerge([color, spacing, textDecoration, animation, "p-8" ])}
      >
        Tailwind Button
      </button>
Tailwind Mergeを使ったクラス名結合の例

 Tailwind Mergeを使えば、p-8を指定したとき、先行するpx-4とpy-2を削除した上で結合してくれるので以下のように意図するようになります。

デメリット3:クラス名選択処理の煩雑さ

 これは積極的にコンポーネント化していくときに直面する問題ですが、バリエーションのあるコンポーネントを定義しようとしたときクラス名の構築処理が煩雑になりがちです。例えば、サイズのバリエーション「sm, md, lg」のどれかを指定でき、かつボタン種別の「primary」と「danger」で見た目を変えることができるボタンを定義しようとすると、以下のような煩雑なコードになります。

  const { size, variant } = props;
    :
  const buttonClass = `rouded bg-slate-950 ${
    size === "sm" ? "px-2 py-1" : size === "lg" ? "px-6 py-4" : "px-4 py-2"
  } ${variant === "danger" ? "text-red-400" : "text-red-400"}`;

 これは対応するクラス名文字列を条件によってピックアップする定型的な処理です。

 この問題は、「CVA」(Class Variants Authority)や「Tailwind Variants」というライブラリを使用することで改善することができます。以下はTailwind Variantsを用いた、次回の記事で解説するチュートリアルのサンプルコードからの抜粋です。

import React from "react";
import { tv, type VariantProps } from "tailwind-variants";
export const button = tv({                   (1)
  base: `shadow-neon rounded bg-slate-950 font-bold border-2 `,
  variants: {
    variant: {
      primary: `mb-4 border-blue-400 text-blue-500 hover:bg-blue-700`,
      danger: `border-red-400 text-red-300 hover:bg-red-700"`,
    },
    size: {
      lg: `px-6 py-4`,
      sm: `px-2 py-1`,
      md: `px-4 py-2`,
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "md",
  },
});
type ButtonProps = ComponentProps<"button"> &      (2)
  VariantProps<typeof button> & {
    children: React.ReactNode;
  };
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    { className, variant, size, children, disabled, type = "button", ...props },
    ref,
  ) => {
    return (
      <button
        type={type}
        className={button({ variant, className, size })}    (3)
        ref={ref}
        disabled={disabled}
        {...props}
      >
        {children}
      </button>
    );
  },
);
Button.displayName = "Button";

 上記のコードでは、煩雑になりがちな分岐処理を、(1)でtv()に与える分岐を表すツリーデータで表現し、tvの返り値である関数buttonを(3)でbutton({ variant, className, size })}のように呼び出すことでクラス名を構築しています。また、(2)のようにPropsの型を生成することもできます。

 CVAとTailwind Variantsは同じ目的のライブラリですが、CVAはTailwind CSSに依存しないライブラリであるのに対して、Tailwind VariantsはTailwind CSSに特化しており、前述のTailwind Mergeをデフォルトで内部で呼び出してスタイルの矛盾を防ぎます。また、現在の画面サイズに応じたレスポンシブ対応処理のための機能を持っています。その他に、複数のクラスを同時に定義できるslot機能もあり、CVAより高機能であるといえます。

 次回は、T3 StackでTailwind CSSを活用するチュートリアルを紹介し、App Routerを使ってtRPCを活用するコード例を解説、Server Actionsと比較します。

著者紹介

上原潤二

ソフトウェア開発企業のNTTテクノクロスで、Gatsbyによる社内の技術情報ポータルサイト「Tech+Hub」の開発、運用や、React+TypeScriptを用いた開発支援や研修を担当。T3 Stackサイト日本語版訳者。書籍「プログラミングGroovy」主著者。

中学生以来、言語処理系の実装に興味を持ち続けている。

Twitter:@uehaj


Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。