🦔

Tailwind CSSのクラス属性長くなりがちな問題について

2023/12/11に公開

この記事は、カオナビ Advent Calendar 2023(シリーズ1) 11日目です。

https://qiita.com/advent-calendar/2023/kaonavi

はじめに

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.

https://imperavi.com/books/ui-typography/basis/line-length/#:~:text=Typically 60 to 80 characters,optimal line length increases readability.

なお、Prettierにおいては、1行80文字以下を推奨しています。

For readability we recommend against using more than 80 characters

https://prettier.io/docs/en/options.html#print-width

上述の例では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へと戻れば解決するだけの悩みのようにユーティリティーファーストなアプローチを取らない開発者からは見えるのだろうと思います。

https://twitter.com/stolinski/status/1613699772111638530

Prettierによるクラス属性の自動整形

読みづらさについてはコードフォーマッターによるコード整形で解決される部分もあると思いますが、以前PrettierにおいてHTML上の同じプレフィックスのクラス名毎に改行するバージョンがリリースされて、その後revertされるということもあったようです。

https://github.com/prettier/prettier/issues/10918

過度な改行は、むしろ読みにくさを増大させるところがあるかもしれません。横へ長くしないために改行するとしても、どのような形で改行するのが望ましいかは議論がありそうです。

一貫性のないクラス名

また、クラス属性が長くなることに加えて、クラス名に一貫した規則性がないことも読みづらさを助長しているかもしれません。

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ではそのような内容がやりとりされています。

https://github.com/tailwindlabs/tailwindcss/issues/575

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.

https://tailwindcss.com/docs/reusing-styles#extracting-components-and-partials

テンプレートとスタイルをコロケーションすることがユーティリティーファーストのアプローチの利点を活かすので、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を利用しない

https://twitter.com/adamwathan/status/1226511611592085504

幾つものクラスがHTML上に並ぶのを避ける方法として@applyを利用することができますが、読みづらさを回避するために利用するべきではないとTailwind CSS公式ドキュメントでも言及されています。

https://tailwindcss.com/docs/reusing-styles#avoiding-premature-abstraction

@applyを利用すれば、それと引き換えにユーティリティーファーストの利点を得られなくなり、以下のような問題と向き合う必要があります。

  • クラスの命名
  • HTMLとCSSとのファイル間を行き来する必要性
  • CSSのグローバルスコープ
  • CSSのバンドルサイズ

@applyを多用するような形になるのであれば、Tailwind CSS自体を利用しないことを検討するべきかもしれません。

より厳格に@applyの利用を一切禁止したければ、Stylelintat-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においても議論があるものの実現には至ってなさそうです。

https://github.com/tailwindlabs/tailwindcss/discussions/7763

variant

読み手にとっては、単純に改行されるのではなく構造化された情報となっている方がより理解しやすい形になるでしょう。
Tailwind CSSにおいても、昨今のUIフレームワークやCSS in JSのライブラリーで見られるvariant APIを提供するライブラリーの利用により構造化された形でクラス名を記述できます。
代表的なライブラリーとしてtailwind-variantsがあります。

https://github.com/nextui-org/tailwind-variants

Buttonコンポーネントであれば、複数のバリエーションを持つことが多いと思いますが、バリエーション毎のクラス名をオブジェクト構造で示すことができます。
以下はcolorというPropsでprimarysecondaryのパターンを追加した形の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プラグインで一貫したクラスの並び順にすることができます。

https://tailwindcss.com/blog/automatic-class-sorting-with-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の拡張機能として利用できます。

https://github.com/vechai/tails-devtools

この拡張機能では、対象要素に付与されているTailwind CSSのクラスがTailwind CSSのタブ内にチェックボックス形式のリストとして表示されるので、一覧性高くクラスを確認することができるようになっています。
未使用クラスでも問題なく新たに追加でき、クラス名の補完候補も表示されます。

まとめ

Tailwind CSSにおいてHTMLのクラス属性が長くなりがちな点について、公式ドキュメントに示されているプラクティスやライブラリーなどについて確認してみました。
ユーティリティーファーストのアプローチを取る以上クラスが長くなることをある程度は受け入れるべきかなと思いましたが、小さいスコープへと分けていくことで多少緩和できる部分はあるのかなと思います。
また、エコシステムやコミュニティーが充実していることによる恩恵を得られることが多いのかなと感じました。

参考

GitHubで編集を提案

Discussion