Tailwind CSSのクラス属性長くなりがちな問題について
この記事は、カオナビ Advent Calendar 2023(シリーズ1) 11日目です。
はじめに
Tailwind CSSというと、ユーティリティーファーストのCSSフレームワークとして非常に人気がありますが、一方で、批判的な意見を度々目にすることもあるフレームワークです。
批判的な意見の中でもよく目にするのは、クラス属性に膨大な量のクラスが指定されることでHTMLが読みづらいという類の指摘だと思います。
ここでは、Tailwind CSSを利用しているプロジェクトにおけるクラスが長すぎることによる読みづらさをどのようにして緩和できるかを確認してみようと思います。
クラスが横に長くなる
Tailwind CSSに限らずユーティリティーファーストのCSSフレームワークであれば、HTMLとCSSのファイル間を行き来することなく同じ場所でスタイリングできる利点が得られる代償としてクラス属性に膨大な量のクラスが並びがちです。
スタイルの記述をCSSからHTMLへ移すので当然のことですが、セマンティックなCSSのアプローチをとってきた開発者からすると理解が容易でないアプローチかと思います。
1つ例を挙げてみると、Reusing Styles - Tailwind CSSに記載のコード例から引用したものが以下になります。
<button class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
Save changes
</button>
button
のクラス属性に幾つものクラスが並んでいることが見て取れます。
ユーティリティーファーストのフレームワークである以上、HTML上に大量のクラスを記述するのは自然なことと思います。それでもこれを読んですぐさまどのようなUIとなるかイメージすることは容易でないでしょう。
ソースコードを上から下へ向かって読む際には、横方向にスクロールが発生するほど長くなることで次の行へ目を移動する負荷も高くなると思います。英文は1行に60から80字以内で収めるのが理想的とされているようなので、それがHTMLを読む上でも適用されるなら横方向へクラス名が並び続けるのは避けたいはずです。
Typically 60 to 80 characters per line are the ideal line length for text in English for any UI.
なお、Prettierにおいては、1行80文字以下を推奨しています。
For readability we recommend against using more than 80 characters
上述の例ではbutton
要素の部分のみですが、実際の開発において読むことになるのは、より多くのHTML要素を含むものばかりでしょう。表現したいUIやソースコードの書き方次第でこのコード例よりもっと読みづらくなることがほとんどかと思います。
以下のように、HTMLとは切り離してCSSファイルに記述すれば、上述のような1行でHTMLのコンテンツに混ぜて記述するよりもずっと読みやすいということが、よく言及されると思います。
/*
* 以下は、ざっくり上述のクラスをCSSに置き換えたもの。カスタムプロパティーの定義は省略。
* HTMLは <button class="btn">Save changes</button> のみになる。
*/
.btn {
padding: 0.5rem 1rem;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
color: rgb(255 255 255 / var(--tw-text-opacity));
font-weight: 600;
border-radius: 0.5rem;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.btn:hover {
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.btn:focus {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
outline: 2px solid transparent;
outline-offset: 2px;
}
HTML上でクラスが冗長になることを思い悩むのは、CSSへと戻れば解決するだけの悩みのようにユーティリティーファーストなアプローチを取らない開発者からは見えるのだろうと思います。
Prettierによるクラス属性の自動整形
読みづらさについてはコードフォーマッターによるコード整形で解決される部分もあると思いますが、以前PrettierにおいてHTML上の同じプレフィックスのクラス名毎に改行するバージョンがリリースされて、その後revertされるということもあったようです。
過度な改行は、むしろ読みにくさを増大させるところがあるかもしれません。横へ長くしないために改行するとしても、どのような形で改行するのが望ましいかは議論がありそうです。
一貫性のないクラス名
また、クラス属性が長くなることに加えて、クラス名に一貫した規則性がないことも読みづらさを助長しているかもしれません。
1例としてflexboxやgridにおけるアイテム配置に関するCSSプロパティーとクラス名についてみてみます。justify-content
のユーティリティークラスはjustify-
から始まるクラス名です。align-items
のユーティリティークラスはalign-
から始まるのでなく、先頭にはitems-
が入ります。
これを対応表にしてみると、以下のようになり、ユーティリティークラス名が推測しやすくなるような規則性を持たないことが窺えます。
CSSのプロパティー | Tailwind CSSのユーティリティークラス |
---|---|
justify-content |
justify-* |
justify-items |
justify-items-* |
justify-self |
justify-self-* |
align-content |
content-* |
align-items |
items-* |
align-self |
self-* |
CSSプロパティーを覚えている開発者は、CSSプロパティー名から推測するクラス名と一致しないケースが多々あり、混乱するかもしれません。一方で似通った部分が多いCSSフレームワークを利用した経験を持っている開発者であれば、懸念が少ないのかもしれません。
CSSのプロパティー名とその値の組み合わせによるクラス名で全て統一されていれば、クラス名とその実体として持つスタイルが推測しやすいかもしれません。
ただ、そうするとただでさえ横方向に長くなりがちなクラスの羅列がさらに長くなってしまうことが懸念として考えられます。以下のissueではそのような内容がやりとりされています。
Tailwind CSSの開発者であるAdam Wathan氏は、ブラウザ上でデザインできるようなフレームワークとなるよう、特に利用頻度が高いクラス名には簡潔で短い命名を重視したことをコメントしています。
Brevity was definitely an important element, especially for things that are used a lot like margin and padding classes. One of the things I want Tailwind to be good for is designing in the browser.
https://github.com/tailwindlabs/tailwindcss/issues/575#issuecomment-460103788
コアプラグインを無効化して自身で一貫した命名のクラス名によるプラグインを追加するというアプローチも可能ですが、実際にそのようなアプローチを取るTailwind CSSユーザーは極めて稀でしょう。
クラスが長くなることによる読みづらさを緩和するには
では、このような読みづらさの要因となるクラスが長くなることに対してどのような回避策がとれるでしょうか。
CSSの抽象化よりも常にコンポーネントの抽出を優先する
幾つものクラスが並んでいる状態を緩和するには、コンポーネントを適宜分けることがまず肝要だと考えられます。
Tailwind CSSの公式ドキュメントのスタイルの再利用に関するセクションで、以下のようにコンポーネントやテンプレート機能を持つフレームワークなどの利用を推奨しています。
If you need to reuse some styles across multiple files, the best strategy is to create a component if you’re using a front-end framework like React, Svelte, or Vue, or a template partial if you’re using a templating language like Blade, ERB, Twig, or Nunjucks.
テンプレートとスタイルをコロケーションすることがユーティリティーファーストのアプローチの利点を活かすので、CSSの抽象化は避けてコンポーネントに切り出すのが適切だと考えられます。
前出のbutotn
の例であれば、.btn
クラスを用意して抽象化するのではなく、以下のようにコンポーネントを切り出して、ユーティリティークラスをそのまま使用するべきということになります。
function Button({ children }) {
return (
<button className="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
{children}
</button>
);
}
Button
コンポーネント内部では多くのクラス名が並びますが、Button
コンポーネントを利用する側では<Button>Save changes</Button>
という形になり、冗長なクラス属性が見えなくなります。
同じように全体通して適宜コンポーネント化することで、クラス名で埋め尽くされる状況はいくらか緩和できそうです。
@apply
を利用しない
幾つものクラスがHTML上に並ぶのを避ける方法として@apply
を利用することができますが、読みづらさを回避するために利用するべきではないとTailwind CSS公式ドキュメントでも言及されています。
@apply
を利用すれば、それと引き換えにユーティリティーファーストの利点を得られなくなり、以下のような問題と向き合う必要があります。
- クラスの命名
- HTMLとCSSとのファイル間を行き来する必要性
- CSSのグローバルスコープ
- CSSのバンドルサイズ
@apply
を多用するような形になるのであれば、Tailwind CSS自体を利用しないことを検討するべきかもしれません。
より厳格に@apply
の利用を一切禁止したければ、Stylelintのat-rule-disallowed-list
ルールを用いて静的解析で防ぐことができると思います。
ただ、Tailwind CSSを使ってユーティリティーファーストのコンセプトを重視するのであればCSSを書くことがあまり多くないと思うので、僅かな量のCSSのためにStylelintを入れるのは過剰な対応かもしれません。
バリエーション毎に分類する
コンポーネントを分けて全体的な見通しを改善できるとしても、個々のコンポーネント内におけるクラスの羅列はまだ改善が必要でしょう。
論理的なグループで適宜改行を入れると、1行に全てのクラスが並ぶよりも把握しやすくなるかもしれませんが、このようなフォーマットを手動で行うのは骨が折れる作業です。
return (
<button className="
py-2 px-4
bg-blue-500 text-white font-semibold rounded-lg shadow-md
hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75
">
{children}
</button>
);
Prettierのようなフォーマッターで対応できると良いかもしれませんが、先に触れたようにPrettierには過去の対応がrevertされていて、Tailwind CSSにおいても議論があるものの実現には至ってなさそうです。
variant
読み手にとっては、単純に改行されるのではなく構造化された情報となっている方がより理解しやすい形になるでしょう。
Tailwind CSSにおいても、昨今のUIフレームワークやCSS in JSのライブラリーで見られるvariant APIを提供するライブラリーの利用により構造化された形でクラス名を記述できます。
代表的なライブラリーとしてtailwind-variantsがあります。
Button
コンポーネントであれば、複数のバリエーションを持つことが多いと思いますが、バリエーション毎のクラス名をオブジェクト構造で示すことができます。
以下はcolor
というPropsでprimary
、secondary
のパターンを追加した形のButton
コンポーネントの例になります。
import { tv } from "tailwind-variants";
const button = tv({
base:
"py-2 px-4 text-white font-semibold rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-opacity-75",
variants: {
color: {
primary: "bg-blue-500 hover:bg-blue-700 focus:ring-blue-400",
secondary: "bg-purple-500 hover:bg-purple-700 focus:ring-purple-400",
},
},
});
// <Button color='primary'>...</Button> のように利用する
function Button({ variant, children }) {
return (
<button className={button({ color: variant })}>
{children}
</button>
);
}
base
に指定されるクラスがまだ冗長であることは確かです。簡単に考えるのであればbase
のクラスを論理的なグループで改行を入れて分けるような形にするとさらに見通しをよくできるかもしれません。
また、slots
キーを用いて、1つの要素に限らず複数要素で構成されるコンポーネントも構造化した形にまとめることができます。
クラスの並び順に一貫性を持たせる
クラスが長くなることを全面的に避けることは難しいかと思いますが、並び順に規則性があるのと無いのとでは読みづらさに大きな違いがあるかなと思います。Tailwind CSS公式のPrettierプラグインで一貫したクラスの並び順にすることができます。
並び順をカスタマイズするオプションは用意されておらず、以下の並び順でフォーマットされることになると思います。
順番 | 種類 | 備考 |
---|---|---|
0 | Tailwind CSSとは関係のないクラス | |
1 | base |
プラグインでこのレイヤーにクラスを追加しない限り、リセットCSS相当のpreflight のみが該当するかと思うので、デフォルトの状態では該当するクラスが無い認識。 |
2 | components |
|
3 | utilities |
上書きするクラスがより後方へ並び、意図せずスタイルが上書きされないようにしている。概ねボックスモデルに基づいて、レイアウトへの影響があるものを前方に、装飾のものを後方へという順序。 |
4 |
:hover のようなmodifiers |
|
5 |
:md のようなレスポンシブmodifiers |
また、このプラグインを利用する上での留意点として、Prettierの設定のプラグインを列挙する中の最後尾へ記載する必要があります。
"plugins": [
...,
...,
"prettier-plugin-tailwindcss"
]
DevTools
クラスが大量に並ぶ状態を目にするのはエディター上に限らず、ブラウザー上でUIを確認する際にDevToolsからということもあると思います。
コンポーネントを分けてもvariant APIを利用しても、最終的にDevToolsで要素を検証すると大量のクラスがHTML全体にわたって並んでいる状態になります。
また、Tailwind CSSはJITにより使用しているクラスのみをビルド結果に含めるので、使用していないクラスをブラウザー上で追加して確認するということが困難かと思います。
safelist: [{ pattern: /.*/ }]
のような設定を追加して全てのクラスをビルド結果に含むことで事実上JITを無効化するようなこともできるようですが、ブラウザーにおける確認のためにこのような設定を追加したくはないかと思います。
このDevToolsの課題については、既に解決するための拡張機能がいくつかあるのでそれを利用するのが良いかもしれません。例えばTailwind CSS DevtoolsはOSSでChromeおよびFirefoxの拡張機能として利用できます。
この拡張機能では、対象要素に付与されているTailwind CSSのクラスがTailwind CSS
のタブ内にチェックボックス形式のリストとして表示されるので、一覧性高くクラスを確認することができるようになっています。
未使用クラスでも問題なく新たに追加でき、クラス名の補完候補も表示されます。
まとめ
Tailwind CSSにおいてHTMLのクラス属性が長くなりがちな点について、公式ドキュメントに示されているプラクティスやライブラリーなどについて確認してみました。
ユーティリティーファーストのアプローチを取る以上クラスが長くなることをある程度は受け入れるべきかなと思いましたが、小さいスコープへと分けていくことで多少緩和できる部分はあるのかなと思います。
また、エコシステムやコミュニティーが充実していることによる恩恵を得られることが多いのかなと感じました。
Discussion