CSSのユーティリティクラスと「関心の分離」——いかにしてユーティリティファーストにたどり着いたか(翻訳)
Tailwind CSS作者のAdam Wathan氏による「CSS Utility Classes and "Separation of Concerns"」の日本語訳です。翻訳に当たって原著者の許諾を得ています。
2021年10月29日に全文再翻訳しました。
この数年の間で、私のCSSの書き方は、非常に「セマンティック」なアプローチから「ファクショナルCSS」と呼ばれるものに変わりました。
この書き方でCSSを書くと、多くの開発者からかなりの反感を買うことがあります。そのため、私がいかにしてここまでたどり着いたかを説明することで、その過程で得た教訓や洞察について共有したいと思います。
第1段階 「セマンティック」なCSS
よいCSSのためのベストプラクティスとして、耳にするであろうことのひとつは「関心の分離」です。
考え方としては、HTMLにはコンテンツについての知識のみを含めるべきであり、スタイルの規定はすべてCSSの中で行わなければならないというものです。
次のHTMLを見てください。
<p class="text-center"> Hello there! </p>
.text-center
クラスが見えますね? テキストの中央揃えはデザインの規定であるため、このコードは「関心の分離」に反します。スタイルの知識がHTMLに漏れ出てしまっているのです。
代わりに推奨されるアプローチは、コンテンツに基づいたクラス名を要素に付与し、それらのクラスをCSSのフックにしてマークアップにスタイルを設定することです。
<style> .greeting { text-align: center; } </style> <p class="greeting"> Hello there! </p>
このアプローチの真骨頂がCSS Zen Gardenです。「関心を分離」しさえすれば、スタイルシートを入れ替えるだけで、サイトを完全に再構築できることを示すために設計されたのです。
ワークフローは次のような感じになります。
1. 新しく作るUI(この場合は著者略歴(author bio)カード)のマークアップをする。
<div> <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt=""> <div> <h2>Adam Wathan</h2> <p> Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut. </p> </div> </div>
2. コンテンツに基づいた説明的なクラスを1、2個追加する。
- <div> + <div class="author-bio"> <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt=""> <div> <h2>Adam Wathan</h2> <p> Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut. </p> </div> </div>
3. マークアップにスタイルを適用するために、これらのクラスをCSSやLess、Sassの「フック」として用いる。
.author-bio { background-color: white; border: 1px solid hsl(0,0%,85%); border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; > img { display: block; width: 100%; height: auto; } > div { padding: 1rem; > h2 { font-size: 1.25rem; color: rgba(0,0,0,0.8); } > p { font-size: 1rem; color: rgba(0,0,0,0.75); line-height: 1.5; } } }
最終的には次のデモのようになります。
See the Pen Author Bio, nested selectors by Adam Wathan (@adamwathan) on CodePen.
このアプローチは理解しやすく、筋が通っていると思ったので、しばらくはこのようにHTMLとCSSを書いていました。
しかし、そのうちなにか違和感を覚え始めます。
「関心を分離」しても、CSSとHTMLは明らかに結びついていたのです。ほとんどのCSSがマークアップと合わせ鏡のようでした。入れ子になったCSSセレクタに、HTMLの構造がそのまま反映されてしまっていました。
マークアップはスタイルの規定について関心を持ちませんでしたが、CSSはマークアップの構造に関心を持っていました。
結局のところ、関心は分離できていなかったのでしょう。
第二段階 スタイルを構造から切り離す
この結びつきを切り離す方法を探し回った結果、行き着いたのは、マークアップにより多くのクラスを追加して、直接要素を選択できるようにする――セレクタの詳細度を低く保ち、CSSを特定のDOM構造に依存させないようにするという解決策でした。
こうした考え方を提唱する方法論として、最も有名なのがBlock Element Modifier――略してBEMです。
BEMらしいアプローチを取ると、著者略歴のマークアップは次のようになります。
<div class="author-bio"> <img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt=""> <div class="author-bio__content"> <h2 class="author-bio__name">Adam Wathan</h2> <p class="author-bio__body"> Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut. </p> </div> </div>
そして、CSSは次のようになります。
.author-bio { background-color: white; border: 1px solid hsl(0,0%,85%); border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .author-bio__image { display: block; width: 100%; height: auto; } .author-bio__content { padding: 1rem; } .author-bio__name { font-size: 1.25rem; color: rgba(0,0,0,0.8); } .author-bio__body { font-size: 1rem; color: rgba(0,0,0,0.75); line-height: 1.5; }
これはかなりの進歩だと感じました。マークアップは「セマンティック」なままで、スタイルを規定していません。CSSはマークアップの構造から切り離されているように思えますし、加えて、セレクタの不用意な詳細度に悩まされずに済みます。
しかし、私はジレンマに陥るのです。
似たようなコンポーネントの扱い
サイトの新しい機能として、記事の概要をカードレイアウトで表示する機能を追加するとしましょう。
記事概要(article preview)カードの中には、上部に幅いっぱいの画像が、下部に余白を伴うコンテンツセクションが含まれます。太字のタイトルと、小さく本文テキストもあります。
これが、著者略歴とまったく同じ見た目だとします。
あくまで関心は分離されたままにしつつ、どのように対処するのが最適でしょうか?
記事概要に.author-bio
クラスを使用することはできません。もはやセマンティックではなくなってしまうからです。したがって、このコンポーネントのために.article-preview
を作らざるを得ません。
マークアップは次のようになります。
<div class="article-preview"> <img class="article-preview__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt=""> <div class="article-preview__content"> <h2 class="article-preview__title">Stubbing Eloquent Relations for Faster Tests</h2> <p class="article-preview__body"> In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality. </p> </div> </div>
では、CSSはどのように取り扱うべきでしょうか?
選択肢1 スタイルを複製する
ひとつのアプローチは、単純に.author-bio
のスタイルを複製しつつクラス名を変更することです。
.article-preview { background-color: white; border: 1px solid hsl(0,0%,85%); border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .article-preview__image { display: block; width: 100%; height: auto; } .article-preview__content { padding: 1rem; } .article-preview__title { font-size: 1.25rem; color: rgba(0,0,0,0.8); } .article-preview__body { font-size: 1rem; color: rgba(0,0,0,0.75); line-height: 1.5; }
これでもうまくいきますが、当然まったくDRYではありません。それに、これらのコンポーネントはわずかに違う道に逸れやすくなり(異なるパディングや文字色になるなど)、デザインの一貫性が失われることになります。
選択肢2 著者略歴コンポーネントを@extend
する
別のアプローチとしては、好みのプリプロセッサーの@extend
機能を使って、すでに.author-bio
コンポーネントとして定義されたスタイルを参照することができます。
.article-preview { @extend .author-bio; } .article-preview__image { @extend .author-bio__image; } .article-preview__content { @extend .author-bio__content; } .article-preview__title { @extend .author-bio__name; } .article-preview__body { @extend .author-bio__body; }
@extend
の使用は一般的には推奨されませんが、それはさておき、問題は解決できたように思いますよね?
CSSから重複を取り除いていますし、マークアップはスタイルを規定していません。
しかし、もうひとつの選択肢についても考えてみましょう。
選択肢3 コンテンツに依存しないコンポーネントを作成する
「セマンティック」な観点では、.author-bio
コンポーネントと.article-preview
コンポーネントにはなんの共通点もありません。ひとつは著者の略歴であり、ひとつは記事の概要です。
しかしこれまで見てきたように、デザインの観点では大いに共通しています。
そのため、共通する性質にちなんだ新しいコンポーネントを作成してもよいでしょう。そうすれば、両方の種類のコンテンツで利用できるようになります。
これを.media-card
と呼びましょう。
CSSは次のようになります。
.media-card { background-color: white; border: 1px solid hsl(0,0%,85%); border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .media-card__image { display: block; width: 100%; height: auto; } .media-card__content { padding: 1rem; } .media-card__title { font-size: 1.25rem; color: rgba(0,0,0,0.8); } .media-card__body { font-size: 1rem; color: rgba(0,0,0,0.75); line-height: 1.5; }
著者略歴のマークアップは次のようになります。
<div class="media-card"> <img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt=""> <div class="media-card__content"> <h2 class="media-card__title">Adam Wathan</h2> <p class="media-card__body"> Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut. </p> </div> </div>
そして、記事概要のマークアップは次のようになります。
<div class="media-card"> <img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt=""> <div class="media-card__content"> <h2 class="media-card__title">Stubbing Eloquent Relations for Faster Tests</h2> <p class="media-card__body"> In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality. </p> </div> </div>
このアプローチではCSSの重複もなくせますが、しかし、これでは「関心の混合」ではないでしょうか?
この瞬間から、これらのコンテンツ両方をメディアカードとしてスタイリングするという知識がマークアップに含まれてしまっています。ではもし、記事概要コンポーネントの見た目を変更せずに、著者略歴コンポーネントの見た目を変更したくなった場合にはどうすればよいでしょうか?
これまでは、スタイルシートを開いて、ふたつのコンポーネントのどちらかに新しいスタイルを適用するだけでした。それが今では、HTMLを編集しなければならなくなってしまったのです! なんということでしょう!
しかし、別の側面からも考えてみます。
もし新しい種類のコンテンツを追加することになって、それにもまた同じスタイリングが必要だとすればどうしましょう?
「セマンティック」なアプローチでは、まずHTMLを記述し、コンテンツ固有のクラスをスタイリングのための「フック」としていくつか追加し、スタイルシートを開き、新しい種類のコンテンツのためのCSSコンポーネントを作成し、そして共通のスタイルを複製するか、@extend
やMixinを使って割り当てます。
コンテンツに依存しない.media-card
クラスでは、記述するのは新しいHTMLのみで、スタイルシートを開く必要はまったくありません。
もし本当に「関心の混合」をしているなら、複数の箇所に変更を加える必要が出てくるのではないでしょうか?
「関心の分離」は論証上の誤り
HTMLとCSSの関係性について「関心の分離」の観点から考えると、白黒は非常にはっきりしています。
「関心の分離」ができている(よい!)か、できていない(悪い!)かだけです。
しかしこれは、HTMLとCSSについて考える上では正しい方法ではありません。
代わりに、「依存の方向」について考えてみましょう。
HTMLとCSSにはふたつの書き方があります。
「関心の分離」
HTMLに依存するCSSコンテンツに基づいたクラス名(
.author-bio
など)を付与することで、CSSがHTMLに依存するように見なせます。HTMLは依存していません。どのような見た目になるかは意識せず、
.author-bio
のようなHTML自身が制御できるフックを公開しているだけです。一方、CSSは依存しています。HTMLがどのようなクラスを公開しているのかを知った上で、それらを介してHTMLにスタイルを設定する必要があります。
このモデルでは、HTMLのスタイル変更が可能になる代わりに、CSSは再利用できません。
「関心の混合」
CSSに依存するHTMLUIの繰り返しのパターン(
.media-card
など)にちなんで、コンテンツにとらわれないようにクラスを命名することで、HTMLがCSSに依存するように見なせます。CSSは依存していません。どのようなコンテンツに適用されるかは意識せず、マークアップに適用できる一連のブロックを公開しているだけです。
HTMLは依存しています。CSSから提供されたクラスを利用しているので、目的とするデザインを実現するためには、どのようなクラスが存在するかを知った上で、必要に応じてそれらを組み合わせる必要があります。
このモデルでは、CSSには再利用性がありますが、HTMLのスタイル変更はできません。
CSS Zen Gardenが最初のアプローチを取る一方で、BootstrapやBulmaのようなUIフレームワークはふたつ目のアプローチを取ります。
本質的にはどちらも間違っていません。特定の状況下において、なにがより重要であるかに基づいて判断される問題です。
あなたが取り組んでいるプロジェクトでは、スタイル変更できるHTMLと再利用性のあるCSSのどちらに価値があるでしょうか?
再利用性の選択
転機が訪れたのは、ニコラス゠ギャラガー氏の「HTMLのセマンティクスとフロントエンドアーキテクチャ」を読んだときでした。
彼の指摘のすべてをここで繰り返すつもりはありませんが、そのブログ記事を読んで確信したのは、私が手がけているようなプロジェクトでは、CSSの再利用性に舵を切ることが明らかに正しい選択だということです。
第三段階 コンテンツに依存しないCSSコンポーネント
この時点での私の目標は、コンテンツに基づいたクラスの作成を明確に避けることです。代わりに、できるだけ再利用性しやすい名前をつけるようにしました。
たとえば次のようなクラス名です。
.card
.btn
、.btn--primary
、.btn--secondary
.badge
.card-list
、.card-list-item
.img--round
.modal-form
、.modal-form-section
という具合です。
再利用性の高いクラスの作成に注力するようになってから、あることに気づきました。
コンポーネントがより多くのことをしようとすればするほど、あるいはコンポーネントが特有のものであればあるほど、再利用しづらくなります。
直感的な例を挙げてみましょう。
フォームを作っているとして、中にはいくつかのセクションがあり、下部に送信ボタンがあるとします。
フォームのコンテンツがすべて.stacked-form
コンポーネントの一部だと考えると、送信ボタンには.stacked-form__button
のようなクラスを付与できます。
<form class="stacked-form" action="#"> <div class="stacked-form__section"> <!-- ... --> </div> <div class="stacked-form__section"> <!-- ... --> </div> <div class="stacked-form__section"> <button class="stacked-form__button">Submit</button> </div> </form>
しかしもしかすると、フォームの一部ではない別のボタンが含まれていて、同じようにスタイルを設定する必要があるかもしれません。
このボタンに.stacked-form__button
クラスを使用すると辻褄が合いません。スタックドフォーム(stacked form)の一部ではないからです。
いずれにしてもボタンは、各ページにおける主要なアクションです。そのため、コンポーネントの共通点に基づいた名前として.btn--primary
と呼ぶことにして、.stacked-form__
という接頭辞を取り払ってしまうのはどうでしょう?
<form class="stacked-form" action="#"> <!-- ... --> <div class="stacked-form__section"> - <button class="stacked-form__button">Submit</button> + <button class="btn btn--primary">Submit</button> </div> </form>
加えて、このスタックドフォームを、浮遊するカードのように見せたいとします。
ひとつのアプローチとしては、モディファイアを作成してこのフォームに適用することです。
- <form class="stacked-form" action="#"> + <form class="stacked-form stacked-form--card" action="#"> <!-- ... --> </form>
しかしすでに.card
クラスがあるのであれば、既存のカードとスタックドフォームを組み合わせて、この新しいUIを構成してみてはどうでしょうか?
+ <div class="card"> <form class="stacked-form" action="#"> <!-- ... --> </form> + </div>
このようなアプローチを取ることで、どんなコンテンツにも対応できる.card
と、どんなコンテナの内側にも配置できる、柔軟な.stacked-form
ができます。
コンポーネントの再利用性が高まり、新たなCSSを記述する必要もありませんでした。
サブコンポーネントよりもコンポジション
たとえばスタックドフォームの下部に別のボタンを追加する必要があり、既存のボタンから少し間隔を空けて配置したいとします。
<form class="stacked-form" action="#"> <!-- ... --> <div class="stacked-form__section"> <button class="btn btn--secondary">Cancel</button> <!-- Need some space in here --> <button class="btn btn--primary">Submit</button> </div> </form>
ひとつのアプローチとして、.stacked-form__footer
のような新しいサブコンポーネントを作成し、.stacked-form__footer-item
のような追加クラスを各ボタンに追加した上で、子孫セレクタを使ってマージンを設定します。
<form class="stacked-form" action="#"> <!-- ... --> - <div class="stacked-form__section"> + <div class="stacked-form__section stacked-form__footer"> - <button class="btn btn--secondary">Cancel</button> - <button class="btn btn--primary">Submit</button> + <button class="stacked-form__footer-item btn btn--secondary">Cancel</button> + <button class="stacked-form__footer-item btn btn--primary">Submit</button> </div> </form>
CSSは次のようになるでしょう。
.stacked-form__footer { text-align: right; } .stacked-form__footer-item { margin-right: 1rem; &:last-child { margin-right: 0; } }
しかし、どこかのサブナビやヘッダーにも同じ問題があるとすればどうでしょう?
.stacked-form
の外側で.stacked-form__footer
を再利用することはできないので、ヘッダーの内側にも新しいサブコンポーネントを作成することになるかもしれません。
<header class="header-bar"> <h2 class="header-bar__title">New Product</h2> + <div class="header-bar__actions"> + <button class="header-bar__action btn btn--secondary">Cancel</button> + <button class="header-bar__action btn btn--primary">Save</button> + </div> </header>
しかしそうすると、.stacked-form__footer
コンポーネントを構築したのと同じ労力を、新しい.header-bar__actions
コンポーネントのためにも費やすことになります。
これは最初に出てきた、コンテンツありきのクラス名の問題と同じように思えます。
この問題を解決するひとつの方法は、再利用やコンポジションが容易にできる、別の新しいコンポーネントを作成することです。
たとえば.actions-list
のようなものです。
.actions-list { text-align: right; } .actions-list__item { margin-right: 1rem; &:last-child { margin-right: 0; } }
これで.stacked-form__footer
コンポーネントと.header-bar__actions
コンポーネントを完全に取り払って、代わりに両方の場面で.actions-list
を使用できるようになりました。
<!-- Stacked form --> <form class="stacked-form" action="#"> <!-- ... --> <div class="stacked-form__section"> <div class="actions-list"> <button class="actions-list__item btn btn--secondary">Cancel</button> <button class="actions-list__item btn btn--primary">Submit</button> </div> </div> </form> <!-- Header bar --> <header class="header-bar"> <h2 class="header-bar__title">New Product</h2> <div class="actions-list"> <button class="actions-list__item btn btn--secondary">Cancel</button> <button class="actions-list__item btn btn--primary">Save</button> </div> </header>
しかし、これらアクションリスト(actions list)の一方を左揃えに、もう一方を右揃えにしたいとすればどうでしょう? .actions-list--left
と.actions-list--right
モディファイアを作るのでしょうか?
第四段階 コンテンツに依存しないコンポーネント+ユーティリティクラス
絶え間なくこのようなコンポーネントの名前を考え続けていると疲れ果ててしまいます。
.actions-list--left
のようなモディファイアを作るということは、ひとつのCSSプロパティを割り当てるためだけにひとつのコンポーネントを新しく作るということです。名前にleft
と含まれている以上、「セマンティック」であると惑わされることもないでしょう。
もし、別のコンポーネントでも左揃えと右揃えのモディファイアが必要になれば、同様に新しいモディファイアを作成するのでしょうか?
こうして、.stacked-form__footer
と.header-bar__actions
を廃止して、ただひとつの.actions-list
と置き換える判断をしたときに直面した問題に戻ってきます。
重複よりもコンポジションを選びます。
では、アクションリストのひとつは左揃えに、もうひとつは右揃えにしたいとき、コンポジションを使えばどのように問題を解決できるでしょうか?
配置ユーティリティ
コンポジションを用いてこの問題を解決するには、再利用可能で、かつ必要な効果を得られるクラスを追加しなければいけません。
モディファイアのことはすでに.actions-list--left
と.actions-list--right
と呼ぶことにしていたので、これから作る新しいクラスを.align-left
や.align-right
のように呼ばない手はありません。
.align-left { text-align: left; } .align-right { text-align: right; }
これでコンポジションを用いて、スタックドフォームのボタンを左揃えにできるようになりました。
<form class="stacked-form" action="#"> <!-- ... --> <div class="stacked-form__section"> <div class="actions-list align-left"> <button class="actions-list__item btn btn--secondary">Cancel</button> <button class="actions-list__item btn btn--primary">Submit</button> </div> </div> </form>
そして、ヘッダーのボタンは右揃えに。
<header class="header-bar"> <h2 class="header-bar__title">New Product</h2> <div class="actions-list align-right"> <button class="actions-list__item btn btn--secondary">Cancel</button> <button class="actions-list__item btn btn--primary">Save</button> </div> </header>
不安がらないで
HTMLの中にある「left」と「right」という表現を見て不安になるかもしれません。しかし確認しておきたいのは、もうしばらく前から、視覚的なパターンにちなんだコンポーネント名を使っているということです。
.stacked-form
が.align-right
よりも「セマンティック」であるということはありません。いずれもマークアップに属するプレゼンテーションにどのように影響を与えるかにちなんで名付けられたもので、特定の形のプレゼンテーションを実現するためにこれらのクラスをマークアップに適用するのです。
つまり、CSSに依存したHTMLを書いています。フォームを.stacked-form
から.horizontal-form
に変更したければ、CSSではなくマークアップで行います。
不要な抽象化を削除する
この解決策において興味深いのは、.actions-list
コンポーネントが根本的に使い物にならなくなったことです。その以前はコンテンツを右揃えにするためだけのものでした。
これを削除してみましょう。
- .actions-list { - text-align: right; - } .actions-list__item { margin-right: 1rem; &:last-child { margin-right: 0; } }
しかし、.actions-list
がないのに.actions-list__item
があるのはちょっと変ですよね。.actions-list__item
コンポーネントを作らずに、元々の問題を解決する方法はほかにないでしょうか?
思い返してみると、このコンポーネントを作ったのは、ふたつのボタンの間に少しのマージンを追加するためでした。.actions-list
は、ボタンのリストを表すのに適切なメタファーであり、総称的かつ十分に再利用可能なものでしたが、もちろん「アクション」ではない項目の間にも同じだけの余白が必要な場面もあるでしょう。
より再利用性しやすい名前にするとすれば、.spaced-horizontal-list
というところでしょうか? すでに、実際にスタイル設定する必要があるのは子要素だけだからという理由で、.actions-list
コンポーネントを削除したばかりなのにもかかわらず。
スペーサーユーティリティ
子要素だけにスタイルが必要なのであれば、手の込んだ擬似セレクタを使ってグループとしてスタイルを設定するのではなく、子要素に個別にスタイルを設定したほうが簡単ではないでしょうか?
要素の隣に余白を追加したいとき、最も再利用性しやすいのは、「この要素の隣には余白ができる」と表現できるクラスです。
すでに.align-left
や.align-right
のようなユーティリティを追加していますし、右方向のマージンを追加するためだけのユーティリティも新しく作るのはどうでしょうか?
.mar-r-sm
のような新しいユーティリティクラスを作成して、要素の右側にわずかなマージンを追加してみましょう。
- .actions-list__item { - margin-right: 1rem; - &:last-child { - margin-right: 0; - } - } + .mar-r-sm { + margin-right: 1rem; + }
フォームとヘッダーは次のようになります。
<!-- Stacked form --> <form class="stacked-form" action="#"> <!-- ... --> <div class="stacked-form__section align-left"> <button class="btn btn--secondary mar-r-sm">Cancel</button> <button class="btn btn--primary">Submit</button> </div> </form> <!-- Header bar --> <header class="header-bar"> <h2 class="header-bar__title">New Product</h2> <div class="align-right"> <button class="btn btn--secondary mar-r-sm">Cancel</button> <button class="btn btn--primary">Save</button> </div> </header>
.actions-list
の概念はもはやどこにも見当たらず、CSSは小さくなり、クラスの再利用性は高まりました。
第五段階 ユーティリティファーストCSS
これが腑に落ちると、一般的なビジュアル調整のために必要なユーティリティクラス一式を構築してしまうまで時間はかかりませんでした。これは例えば――
- テキストサイズ、色、ウェイト
- ボーダーカラー、幅、基準位置
- 背景色
- フレックスボックスのユーティリティ
- パディングとマージンのヘルパー
これによって、驚くべきことに、新しいCSSを記述することなくまったく新しいUIコンポーネントを構築できるのです。
私のプロジェクトにある一種の「商品カード」コンポーネントを見てみましょう。
マークアップは次のようになっています。
<div class="card rounded shadow"> <a href="..." class="block"> <img class="block fit" src="..."> </a> <div class="py-3 px-4 border-b border-dark-soft flex-spaced flex-y-center"> <div class="text-ellipsis mr-4"> <a href="..." class="text-lg text-medium"> Test-Driven Laravel </a> </div> <a href="..." class="link-softer"> @icon('link') </a> </div> <div class="flex text-lg text-dark"> <div class="py-2 px-4 border-r border-dark-soft"> @icon('currency-dollar', 'icon-sm text-dark-softest mr-4') <span>$3,475</span> </div> <div class="py-2 px-4"> @icon('user', 'icon-sm text-dark-softest mr-4') <span>25</span> </div> </div> </div>
これに使われているクラスの数を見ると、最初は躊躇してしまうかもしれません。しかし、もしこれをユーティリティで構成するのではなく、真のCSSコンポーネントにしたいとすれば、これをなんと呼ぶのでしょうか?
コンテンツありきの名前にすると、コンポーネントは特定のコンテキストでしか使えなくなってしまうので、そうしたくはありません。
すると、こんなところでしょうか?
.image-card-with-a-full-width-section-and-a-split-section { ... }
もちろんあり得ません。それよりも、前に説明したようなもっと簡単なコンポーネントで構成したいと思うでしょう。
では、それはどういったコンポーネントでしょうか?
たとえば、カードというコンポーネントとか。しかし、すべてのカードに影があるわけではないので、.card--shadowed
モディファイアを用意するといいでしょうし、任意の要素に適用できる.shadow
ユーティリティを作成することもできます。そのほうが再利用性しやすそうなので、そうしてみましょう。
サイトにあるカードの中には、角が丸くなっていないものもありますが、このカードは違います。.card--rounded
としてもよいですが、サイトには同じように角が丸くなっている要素がほかにもありますし、それらはカードではありません。.rounded
ユーティリティのほうが再利用しやすいでしょう。
トップの画像はどうでしょう? .img--fitted
のような名前で、カードいっぱいになるかもしれません。サイトでは、親の幅に合わせてなにかをフィットさせたい場所はほかにもいくつかあって、それが画像とは限りません。.fit
ヘルパーのほうがいいかもしれませんね。
そう。私がどこに向かおうとしているか、お分かりになるでしょう。
再利用性に焦点を当てて、この道をずっと辿っていくと、再利用可能なユーティリティを使ってこのコンポーネントを構築するようになるのが自然な流れなのです。
一貫性の強制
小さくて組み合わせ可能なユーティリティを使用する大きな利点は、チームにいるすべての開発者に対して、つねに、固定されたオプションの中から値を選択させられることです。
HTMLのスタイルを設定するとき、「このテキストはもう少し暗くしたほうがいいかな」と思って、ベースとなる$text-color
をdarken()
関数で調整したようなことが何度もあるのではないでしょうか?
あるいは「このフォントはもう少し小さいほうがいいな」と思って、手を入れているコンポーネントにfont-size: .85em
を追加したことはありませんか?
任意の値ではなく、相対的な色や相対的なフォントサイズを使用しているので、「正しい」やり方をしているように感じられることでしょう。
しかし、あなたが自分のコンポーネントのためにテキストを10%暗くする一方で、ほかの人は12%暗くするとすればどうでしょう? 気づいたころには、スタイルシートには402種類もの独自の文字色が存在することになります。
スタイルを設定するたびに新しくCSSを書くことになる場合、コードベースがこのようになる事態は避けられないのです。
- GitLab: 402の文字色、239の背景色、59のフォントサイズ
- Buffer: 124の文字色、86の背景色、54のフォントサイズ
- HelpScout: 198の文字色、133の背景色、67のフォントサイズ
- Gumroad: 91の文字色、28の背景色、48のフォントサイズ
- Stripe: 189の文字色、90の背景色、35のフォントサイズ
- GitHub: 163の文字色、147の背景色、56のフォントサイズ
- ConvertKit: 128の文字色、124の背景色、70のフォントサイズ
新しくCSSを書くことは、真っ白なキャンバスに絵を描くようなもので、好きな値を使うことを妨げるものはなにもありません。
変数やMixinを使って一貫性を持たせることもできますが、そもそも新しく書かれるCSSのすべてが複雑性の元凶なのです。CSSを増やしてもCSSがシンプルになることは決してありません。
代わりに、既存のクラスを適用してスタイルを設定できれば、真っ白なキャンバスの問題はたちまち解消されることになります。
テキスト色の暗さを少し和らげたいですか? .text-dark-soft
クラスを追加しましょう。
フォントサイズを少し小さくする必要がありますか? .text-sm
クラスを使いましょう。
プロジェクトに携わる全員が、選定されたオプションの中からスタイルを決めることで、プロジェクトの規模とともにCSSが直線的に増大してしまう事態を回避できるだけでなく、自ずと一貫性も保たれます。
それでもコンポーネントは作るべきです
ほかのファンクショナルCSSの熱心な支持者と少し違うのは、私は、ユーティリティだけで作るべきだとは考えていないという点です。
ユーティリティベースのフレームワークとして人気のTachyonsなどを見ると、ボタンのスタイルでさえも純粋なユーティリティから作られているのがわかります(Tachyonsはもちろんすばらしいプロジェクトです)。
<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple"> Button Text </button>
ひとまず、これを分解してみましょう。
f6
: フォントサイズをフォントサイズスケールの6番目にする(Tachyonsでは.875rem)br3
: ボーダーラディウスをラディウススケールの3番目にする(.5rem)ph3
: 水平方向のパディングをパディングスケールの3番目のサイズにする(1rem)pv2
: 垂直方向のパディングをパディングスケールの2番目のサイズにする(.5rem)white
: テキストを白くするbg-purple
: 背景色を紫にするhover-bg-light-purple
: ホバーの際には背景色を明るい紫にする
このような同じクラスの組み合わせからなるボタンが複数必要な場合、Tachyonsでは、CSSではなくテンプレートを通して抽象化することが推奨されています。
たとえばVue.jsを使っているのなら、次のように使えるコンポーネントを作ります。
<ui-button color="purple">Save</ui-button>
これは、次のような定義になります。
<template> <button class="f6 br3 ph3 pv2" :class="colorClasses"> <slot></slot> </button> </template> <script> export default { props: ['color'], computed: { colorClasses() { return { purple: 'white bg-purple hover-bg-light-purple', lightGray: 'mid-gray bg-light-gray hover-bg-light-silver', // ... }[this.color] } } } </script>
これは多くのプロジェクトにとって有力なアプローチですが、テンプレートベースのコンポーネントを作成するよりも、CSSコンポーネントを作成したほうが役立つ場面も多いと私は考えています。
私が手がけているようなプロジェクトでは、サイトにある小さなウィジェットをすべてテンプレート化するよりも、新しい.btn-purple
クラスを作って、これら7つのユーティリティをバンドルしたほうがたいてい簡単です。
それでも、最初はユーティリティで作ります
CSSに対して私が取るアプローチをユーティリティファーストと呼んでいるのは、できる限りのものをユーティリティで作ってから、繰り返されるパターンが登場したときにだけ抽出するようにしているからです。
プリプロセッサーとしてLessを使用しているなら、既存のクラスをMixinにできます。つまり、エディタでマルチカーソルを使ってちょっとした操作をするだけで.btn-purple
コンポーネントを作成できます。
残念なのは、SassやStylusでも同じようなことをするには、すべてのユーティリティクラスのために個別のMixinを作成する必要があり、少し手間がかかることです。
もちろん、ユーティリティだけでコンポーネントのすべての宣言が行えるわけではありません。親要素をホバーしたときに子要素のプロパティを変更するような、要素間の複雑なインタラクションは、ユーティリティだけでは困難です。よりシンプルに感じられるやり方を判断して選択するようにしてください。
早すぎる抽象化はもうやめよう
CSSでコンポーネントファーストのアプローチを取ると、たとえ再利用されることがなくてもコンポーネントを作ることになります。この時期尚早の抽象化が原因で、スタイルシートは肥大化したり複雑化することになります。
ナビバーを例に考えてみましょう。アプリにあるメインのナビバーのマークアップは何度も繰り返し記述するでしょうか?
私のプロジェクトでは、メインのレイアウトファイルに一度だけ記述するのが普通です。
まずはユーティリティを使って作るようにして、重複が気になった場合にのみコンポーネントとして抽出するようにすれば、ナビバーをコンポーネントにする必要はおそらくないでしょう。
そして、ナビバーは次のようになります。
<nav class="bg-brand py-4 flex-spaced"> <div><!-- Logo goes here --></div> <div> <!-- Menu items go here --> </div> </nav>
抽出すべきものはなにもありません。
ただのインラインスタイルなのでは?
このアプローチは、HTML要素にスタイル属性を書き殴って必要なプロパティを追加するのと変わらないと考えることも容易でしょう。しかし私の経験では、まったく異なるものです。
インラインスタイルでは、どのような値を選択するかに制約がありません。
ある要素はfont-size: 14px
、別の要素はfont-size: 13px
、また別の要素はfont-size: .9em
、そのまた別の要素はfont-size: .85rem
ということになり得るのです。
つまり、新しいコンポーネントごとに新しいCSSを記述する場合に直面する、真っ白なキャンバスの問題です。
ユーティリティでは選択を迫られます。
これはtext-sm
かtext-xs
か?
py-3
とpy-4
のどちらを使うべきか?
text-dark-soft
とtext-dark-faint
のどちらにしたいのか?
なんでも好きな値を選択することはできず、選定されたリストの中から選ばなければなりません。
380色の文字色ではなく、10色や12色に制限されます。
ユーティリティファーストで作業することは、最初は、コンポーネントファーストよりも直感的ではないかもしれませんが、より一貫したデザインになると経験上言えます。
どこから始めるか
このアプローチに興味を持たれた方は、次のフレームワークについて調べてみるのがいいでしょう。
また最近、私はTailwind CSSというPostCSSフレームワークをオープンソースでリリースしました。実用性を第一に考えながら、繰り返されるパターンから構成要素を抽出するという考え方に基づいて設計しています。
興味のある方は、ぜひTailwind CSSのウェブサイトにアクセスして試してみてください。