普段Sassの人が Tailwind CSS を使って感じるメリットデメリットと、使用上のポイント

普段はWeb制作でSassを書いています。
Web開発案件でTailwind CSSを使う機会があったのですが、衝撃的だったので感じたところを書いていきます。
📖 tailwind3.2.4時点の記事です
tailwind cssとは?
1クラス1スタイルのクラスをHTMLのclass属性にたくさん書いてスタイリングするというものです。
昔CSSが登場する前、HTMLにcolor属性やらalign属性やらがあったなぁ…(←まさかのインラインスタイルですらない)
一瞬、歴史は繰り返すのか…などと思いましたが、現状はかなり違います。最初に書きますが、コーポレートサイトなどのWeb制作には若干不向きに感じています。Reactなどのコンポーネント指向のライブラリと組み合わせて使うと力を発揮するように作られているので、制作の仕方次第では使えることもあるかもしれないです。
普段web制作ではSassを使っています。web開発…特にNext.jsの案件を何件か経験しましたが、CSSをどうすればよいのかの答えがいまだに出ていません。
styled-jsx、styled-components、CSS modules、それらよりも「CSS設計を用いたグローバルCSS」のほうが書きやすい(がCSS設計を共有するという壁がある)と感じていました。
tailwind cssは、全く違う切り口で乗り込んできた異端児な感じがしてます。普段私はユーティリティクラス自体避けているので、最初見た時はあまりいいイメージはなかったのですが、結構流行ってきているのを目にして気になり始めたところ、tailwindを使う機会に恵まれました。
Sassと比べたメリットデメリット
あくまでSassと比べてのものですが、ものすごく良い!と思うところと、つらいと思う点が拮抗しました。
下記箇条書ではパッと見デメリットの方が多いのですが、メリットに挙げた2点が今まで長年悩んできた点なので、それが解決するなんて画期的すぎました。
メリット
- クラス名を考えなくていい
- チームでCSS設計を共有しなくていい
デメリット
- 記述速度に限界がある
- エメットが使えない
- レスポンシブや擬似要素などで長くなると見づらい
- 動的に自由なスタイルを設定できない
慣れれば大丈夫な点(詳しく書きません。さらっと)
- 独自のクラス名を覚える必要がある
→覚える - HTML部分がごちゃごちゃする
→コンポーネントごとにファイルを分けるので気にならなくなる - クラス名がない故に検索視認性が落ちる
→クラス名をきめなくてよい利点を考えれば諦めがつく - デフォルトでは対応していないプロパティが多い
→カスタム修飾子[]の記法でたいていなんとかなる
【メリット】クラス名考えなくて良い、CSS設計をチームで共有しなくて良い
クラス名はCSSと紐づけるキーであり、同時に要素の箇所の意味を表すものでないといけないわけですが、なんでもいいが故に人によって付け方が異なります。
たかがクラス名一つですが、個々の意見や議論が絶えないところでもあります。
このクラス名の命名規則と全体的なルールを決めたものをCSS設計と言いますが、CSS設計をチームで共有するのは結構大変です。
有名どころではBEMやFLOCSSなどがありますが、それとて全ての案件において最適なものではないし、組織的に導入しているなどの理由がない限り、コーダーに学習を強いるものでもありません。それだけベストプラクティスが出しにくい分野であると思います。
そんな状況が何年も続いているので、tailwind cssでCSS設計を共有せずともすぐにプロジェクト進行できるのは画期的でした。また、クラス名を決めなくていいのは思った以上に楽でした。いつもは誤解を生まないよう無難な英単語を考えたり英訳したりする手間があり、それが実はストレスだったんだと気付きました。
【デメリット】記述速度の限界、動的クラス実現不可(一部可能)
記述速度に限界がある
Emmetが使えない
ちなみにエディタはVisual Studio Codeを使っています。
tailwindはclass属性に独自のクラス名を書くので、Emmetは使えません(というかCSSのEmmetを使いません)。
Emmetは、「w100 」タブ と打てば「width:100px;
」が打てるというものです。
エディタの補完機能でも似たようなことができますが、選択する手間がない分補完機能よりもEmmetの方が早いです。
tailwind cssでは基本的に文字列を繋げていく形になるので、「CSSを速く書く」ことは諦めざるをえないと感じます。
ちなみに、Emmetが使えないのはtailwindにかぎらず、styled-jsxやstyled-componentsでもそうでした。
reactのプロジェクトでは基本的に言語モードがTypeScriptReactになり、CSSのEmmetが効かなくなります。(かといって、VS CodeのSettingでTypeScriptReactにCSSのEmmet機能を割り当てると、JSXなどの記述でEmmetが使えない)
marginやwidthなど、基本的なプロパティを書くだけなら逆に省略された文字列で慣れれば早いのかもしれないですが、例えばlinear-gradientのパーセンテージ位置が細かい場合(できればFigmaからコピペしたい)や、その他カスタムスタイルを使わないといけない場合、疑似要素・擬似クラス、メディアクエリなどが重なってくると書くのが辛くなってきます。
レスポンシブや擬似要素などで長くなると見づらい
tailwind cssは、あらかじめメディアクエリが設定されており、それを使っていくことになります。
<!--幅はデフォルトで16、@media (min-width: 768px)で32、@media (min-width: 1024px)で48。-->
<img class="w-16 md:w-32 lg:w-48" src="...">
Sassの場合は1プロパティ一1行ですが、tailwind cssは基本スペース区切りで書いていくので、どうしても見やすさ的には劣ります。メディアクエリの分が既存のものをどう上書きしてるのかがパッと分かりにくく、擬似要素が絡むとさらに読み解くのに時間がかかります。
ただ、レスポンシブ設定に難しいことをする必要がなくすぐ使えるのは良いところだと思います。
一つ個人的に躓くのが、デフォルトのメディアクエリがすべて「min-width」になっているところです。
考え方としてはモバイルファーストです。なので、最初にPC画面を作り、後でスマホ用の画面を作ろうとすると、下記ではNGです。768px以上がw-fullになり、それ以下にw-16が適用されます。
<img class="w-16 md:w-full" src="...">
スマホサイズのみに適用させたい場合は、max-をつけます。
<img class="w-16 max-md:w-full" src="...">
ちなみにmdとlgの間だけ適用したい場合は、<div class="md:max-lg:flex”
といった書き方もできるようです。
PCファーストで慣れているコーダーは気をつけましょう。
動的に自由なスタイルを設定できない
動的に文字列を作りクラス名をつけようとするとうまくいきません。(あくまで「自由なスタイルを」設定できないです。可能なパターンは後述)
例えば下記のような感じでwidthをpropsで渡してw-[hoge]に当てはめようとした場合
import React from "react"
export type Props = {
onClick?: () => void
children: React.ReactNode
width?: string
}
export const Layout = ({ onClick, children, width }: Props) => {
return (
<button onClick={onClick} className={`w-[${width}] bg-white p-2`}>
{children}
</button>
)
}
コンポーネント呼び出し元
<Button width="120px">Hello!</Button>
これで、幅120pxのボタンができるのかと思ったら、できません。
デバッグツールではw-[120px]
がクラスに入っているのが確認できました。つまりcss側でwidth:120px
が生成されていないということです。
これがなぜかというと、公式に書いてありました。
Tailwind CSS works by scanning all of your HTML, JavaScript components, and any other template files for class names, then generating all of the corresponding CSS for those styles.
Tailwind CSS は、すべての HTML、JavaScript コンポーネント、およびその他のテンプレート ファイルをクラス名でスキャンし、それらのスタイルに対応するすべての CSS を生成することによって機能します。
https://tailwindcss.com/docs/content-configuration
さらに
The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files.
If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS:
Tailwind がクラス名を抽出する方法の最も重要な意味は、ソース ファイル内に完全な途切れのない文字列として存在するクラスのみが検出されるということです。
文字列補間を使用したり、部分クラス名を連結したりすると、Tailwind はそれらを見つけられないため、対応する CSS を生成しません。
というわけなので、クラス名を切って一部をpropsで補うことはできないようです。
しかしtailwind cssのクラス名をそのままpropsでわたすのもあまり現実的ではないので、どうするかというと、そこだけtailwindを諦め、インラインスタイルを使うなりグローバルCSSを使うことになります。
個人的には、メディアクエリが不要なもの かつ 単一プロパティはインラインスタイル、メディアクエリ必要なもの or 複数プロパティの場合はグローバル対応かな…と思っています。
動的にクラスを変更したいときに可能なパターン
カスタムスタイルを使った動的クラスは不可ですが、可能なパターンもあります。
1、クラス名をフルで渡す場合
上記の例では、クラス名をw-と[120px]にわけず、propsでそのままw-[120px]
と渡すとCSSが生成されます。
<button onClick={onClick} className={`${width} bg-white p-2`}>
{children}
</button>
<Button width="w-[120px]">Hello!</Button>
2、safelistでCSSを出力する(デフォルトでtailwindがサポートしているものを使う時のみ)
デフォルトでtailwindがサポートしているものを使う場合は、safelistを使えば実際に使っていないCSSも生成することが可能になるので、(使われていないと認識されている)動的な部分にも対応できるようになります。
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
safelist: [
{
pattern: /w-/,
},
],
}
このような感じにしておけば、動的に適用することができます。
<button onClick={onClick} className={`w-${width}`}>
{children}
</button>
<Button width="28">Connect</Button>
使われていないものを全て出力しているので、tailwind公式には「セーフリストは最後の手段」と記載があります。
提供のないプロパティはカスタムスタイルでほぼ解決する
デフォルトで対応していないプロパティでも、カスタムスタイルを使うことによって使用可能になります。これがかなり使い勝手がよいです。[]で囲むだけです。
bg-[url('/img/hero-pattern.svg')]
のようにtailwindとの組み合わせもできます。
mx-[calc(100%-10px)]
のように書けば、calcもそのまま使えます。
プロパティ名がわからなくても、[mask-type:luminance]
と言った具合にプロパティ丸ごと書けるので、このプロパティはtailwindではどう書くのか…みたいなことで悩むことはありません。linear-gradient
のような長くなるものでも、FigmaからCSSをそのままコピーして使うみたいなことができます(下記に注意)。カスタムスタイルは救世主的存在です。
カスタムスタイルを使うときの注意
個人的にすごくハマったのでメモします。
公式に書いてある内容ですが…
どんなプロパティや値も、[ ]
で囲めば適用できると思っていましたが、下記注意が必要です。
- スペースは削除する
- 削除できないスペースはアンダーバーにする
つまり、下記のようなCSSは、
background-image: linear-gradient(180deg,#EFBF03 35%, #FEF4CD 70%, #FEF4CD)
半角スペースを削除し、色コードと%の間(#EFBF03 35%
など)はアンダーバーに変更します。
[background-image:linear-gradient(180deg,#EFBF03_35%,#FEF4CD_70%,#FEF4CD)]
これでやっと効くようになります。(わかっててもミスる。エディタで色分けできたら楽なんですが…)
ちなみに、アンダーバーをアンダーバーとして使いたい場合は、バックスラッシュでエスケープするそうです。
<div class="before:content-['hello\\_world']">
<!-- ... -->
</div>
プロパティの順番が必要になる書き方はできない
通常のCSSは、後ろに書いたプロパティが前のものを上書きします。tailwindは必ずしもそうではないので、例えば一括指定系のプロパティを使っている場合は、意図せず前に書いたものが後ろのものを上書きしてしまう場合があります。
テキストをグラデーションにしたいとき、下記のようなCSSを適用します。
.text-grad {
background: linear-gradient(46.44deg, #f2ca2e 24.03%, #f6eed5 49.73%, #f2ca2e 76.42%);
background-clip: text;
-webkit-text-fill-color: transparent;
}
これをそのままtailwindにすると下記のようになりますが、これだとテキストがclipされませんでした。
<span className="[background:linear-gradient(46.44deg,#F2CA2E_24.03%,#F6EED5_49.73%,#F2CA2E_76.42%)] [-webkit-text-fill-color:transparent] [background-clip:text]">
hoge
</span>
tailwindはプロパティの順番が保証されないので、ビルド後にbackground系の一括指定であるbackgroundプロパティがbackground-clipの後にくると、background-clipを上書きしてしまうからです。
上記の場合は、background
の一括指定を使わず、background-image
〜にすることで解決できます。
<span className="[background-image:linear-gradient(46.44deg,#F2CA2E_24.03%,#F6EED5_49.73%,#F2CA2E_76.42%)] [-webkit-text-fill-color:transparent] [background-clip:text]">
hoge
</span>
ちなみに順序の話でいうと、@layer
を使えばbase、components、utilities の3パターンの順序はbaseから順に保証されているようです。(上記のような細かいパターンよりもっと大枠の話ですが。)
@layer
を使用すると、多少ルールの共有が必要になるので使いませんでしたが、本格的にtailwindを使っていくなら必要な機能なのかもしれません。
【原因不明】同じCSSが読み込まれる
Next.jsと組み合わせた場合に限る(おそらく)と思うのですが、pageごとにcssが生成され、それが全て読み込まれるので検証ツールのスタイルタブがかなり重いです。

同じCSSがpageごとに生成され、その分がすべて読み込まれた状態になっており、さらにそれが2、3個呼び出されているという奇妙な現象です…。

この結果、Chromeの検証ツールが重いです。重すぎて何度か落ちました。
軽いファイルとはいえ読み込まれすぎてエコじゃないし、検証できないのがかなり辛い。
バグかもしれないし何か設定があるのかもしれないし、tailwindが関係ない可能性もあるのですが()、まったく情報がでてこないので未解決です。
まとめ
Web開発でのCSSフレームワークの決定打にはなりませんでしたが、人気があるのもわかると思いました。
近年CSS自体がパワーアップしていて、まだ本格的ではないですがネストやレイヤーに対応してきています。もうSassもなくなってCSSだけで記述する時がくるんじゃないか…と思ったりしてました…………が、そんな中、クラス名をなくしてしまうフレームワークが登場したのはびっくりでした。
CSSの進化の方向性とはかなり違う切り口のフレームワークだと思うので、今後どうなるのか、ある意味楽しみです。