Tailwind CSS作者のAdam Wathan氏による「CSS Utility Classes and "Separation of Concerns"」の日本語訳です。翻訳に当たって原著者の許諾を得ています。
ここ数年の間に私のCSSの書き方は、非常に「セマンティックな」アプローチから「機能本位の(functional)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を作るために必要なマークアップを書きます(この場合は著者の略歴カード):
<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. コンテンツについて叙述するクラスを追加します:
- <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 "Semantic" mapping layer (terrible idea!) by Adam Wathan (@adamwathan) on CodePen.
このアプローチは直感的に納得できたので、しばらくの間はこのようにHTMLやCSSを書いていました。
しかしそのうち少しずつ違和感を覚えはじめました。
私は「関心を分離」していたのですが、CSSとHTMLの間にはまだ明らかな結合がありました。ほとんどの場合において私のCSSはマークアップの鏡のようで、入れ子になったCSSセレクタでHTML構造を完全に反映していました。
私のマークアップはスタイリングの決定に関心を持ちませんでしたが、CSSはマークアップの構造にかなり関心を持っていました。
結局、私の関心はそれほど分離していなかったのかもしれません。
段階2:構造からスタイルを切り離す
この結合に対する解決策を探し回った後に私は、より多くのクラスをマークアップに追加して直接標的にできるようにすること、セレクタの詳細度を低く保ち、CSSを特定のDOM構造に依存させないようにすることなど、より多くの推奨事項を見つけ始めました。
この考え方を提唱するもっとも有名な方法論はBlock Element Modifer、略して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は今ではマークアップ構造から分離されたように感じられます。不必要なセレクタの詳細度を回避できるというおまけつきで。
しかし、私はジレンマに陥りました。
似たようなコンポーネントへの対応
たとえば新しい機能として、カードレイアウトで表示される記事のプレビューをサイトに追加する必要があるとします。
この記事のプレビューカードには、上部に幅いっぱいの画像、その下に余白付きのコンテンツセクション、太字のタイトル、そしていくつかの小さな本文があったとします。
著者の略歴にそっくりだとしましょう。
関心を分離しつつ、どう対処していくのがベストでしょうか?
.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の重複はこのアプローチによっても削除できますが、今は「関心を混合している」のではないでしょうか?
マークアップは思いがけず、これらのコンテンツの両方をメディアカードのようなスタイルにしたいことを知ってしまっています。記事プレビューの見た目は変えずに、著者の略歴の見た目だけを変えたいとしたらどうでしょうか?
以前は、スタイルシートを開いて、2つのコンポーネントのいずれかのために新しいスタイルを決めるだけでした。今ではHTMLを編集する必要があります! 不敬!
でも、ちょっとその裏面について考えてみましょう。
同じスタイリングを必要とする新しいタイプのコンテンツを追加しなければならない場合はどうでしょうか?
「セマンティックな」アプローチを使うと、新しいHTMLを書き、コンテンツ固有のクラスをスタイリングの「フック」として追加し、スタイルシートを開き、新しいコンテンツタイプ用の新しいCSSコンポーネントを作成し、そして共有スタイルを、重複して適用するか@extend
またはmixinを使用して適用します。
コンテンツに依存しない.media-card
クラスを使えば、必要なのは新しいHTMLを書くことだけで、スタイルシートを開く必要はありません。
本当に「関心を混合している」のであれば、複数の箇所に変更を加える必要がないと言えるのでしょうか?
「関心の分離」は論点のすり替え
HTMLとCSSの関係を「関心の分離」という観点で考えてみると白黒は非常にはっきりしています。
関心の分離があるか(良い!)、ない(悪い!)かのどちらかです。
これはHTMLとCSSの正しい考え方ではありません。
その代わり、依存関係の方向を考えます。
HTMLとCSSの書き方は2通りあります:
「関心の分離」
HTMLに依存するCSS。コンテンツにもとづいたクラス名をつけることで(
.author-bio
のように)、HTMLをCSSの依存関係として扱います。HTMLは独立しています。HTMLがどのように見えるかは気にせず、HTMLが統制する
.author-bio
のようなフックを公開しているだけです。一方でCSSは独立したものではありません。HTMLが公開すると決めたクラスを知る必要があり、HTMLのスタイルを整えるためにそれらのクラスを標的にする必要があります。
このモデルではHTMLは再スタイリング可能になりますが、CSSは再利用できません。
「関心の混合」
CSSに依存するHTML。UIのパターンを繰り返した後に、コンテンツに依存しない方法でクラス名をつけると(
.media-card
のように)、CSSはHTMLの依存関係として扱われます。CSSは独立しています。どのようなコンテンツに適用されるかの関心は持たず、マークアップに適用できる構成要素のセットを公開しているだけです。
HTMLは独立していません。CSSによって提供されているクラスを利用しており、その組み合わせによって望ましいデザインを達成するためには、どのようなクラスが存在するかを知る必要があります。
このモデルではCSSは再利用可能になりますが、HTMLは再スタイリングできません。
CSS Zen Gardenは最初のアプローチを取り、BootstrapやBulmaのようなUIフレームワークは2つ目のアプローチを取ります。
本質的にはどちらも「間違っている」のではなく、特定の文脈の中でなにがより重要かを判断しているに過ぎません。
あなたが取り組んでいるプロジェクトでは、再スタイリング可能なHTMLと再利用可能なCSSのどちらにより価値があるでしょうか?
再利用性の選択
転機となったのはNicolas GallagherのAbout HTML semantics and front-end architectureを読んだことです。
ここで彼の指摘をすべて繰り返すつもりはありませんが、私が取り組んでいる種類のプロジェクトにとって、再利用可能なCSSのために最適化することが正しい選択であるとそのブログ記事を読んで確信したのは言うまでもありません。
段階3:コンテンツに依存しない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
と呼べばどうでしょうか?
<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を合成(compose)してみるのはどうでしょうか?
+ <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__footer
を.stacked-form
の外で再利用することはできないので、ヘッダーの中にサブコンポーネントを作ることになるかもしれません:
<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--left
と.actions-list--right
というモディファイアを作るのではないでしょうか?
段階4:コンテンツに依存しないコンポーネント+ユーティリティクラス
これらのコンポーネント名を考えようとするといつも疲労困憊します。
.actions-list--left
のようなモディファイアを作ると、CSSプロパティをひとつ割り当てるためだけにまったく新しいコンポーネントモディファイアを作ることになります。名前にすでにleft
と入っているので、どのみち「セマンティック」だとごまかすこともできないでしょう。
左揃えと右揃えのモディファイアを必要とする別のコンポーネントがあった場合、そのために新しいコンポーネントモディファイアを作成するのはどうでしょうか?
この場合、.stacked-form__footer
と.header-bar__actions
を削除して単一の.action-list
に置き換えることにしたとき直面していた問題に戻ります:
重複よりも合成を選びます。
では2つのアクションリストがあり、ひとつは左揃えにする必要がありもうひとつは右揃えにする必要があるとしたら、合成を用いてどのようにその問題を解決できるでしょうか?
配置ユーティリティ
この問題を解決するためには、望ましい効果を与える再利用可能な新しいクラスをコンポーネントに追加できるようにする必要があります。
すでにモディファイアを.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」という言葉があるのを見て気持ち悪く感じたとすれば、この時点でずっと前からUIの視覚的なパターンにちなんだ名前のコンポーネントを使っていることを思い出してください。
.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
コンポーネントを作成せずにもとの問題を解決する方法はあるでしょうか?
思い返せばこのコンポーネントを作成した理由は、2つのボタンの間に少しのマージンを追加するためでした。.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は小さくなり、クラスの再利用性は高まりました。
段階5:ユーティリティファースト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
ユーティリティの方が再利用しやすいでしょう。
1番上にある画像はどうでしょうか?もしかして、.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の1行1行が新たな複雑さを生み出す機会になっています。より多くのCSSを追加してもあなたのCSSがよりシンプルになることはありません。
代わりに既存のクラスを適用すれば、突然File size キャンバスが真っ白になってしまう問題は解決します。
暗い文字を少し弱めたい場合はどうでしょう? .text-dark-soft
クラスを追加してください。
フォントサイズを少し小さくする必要があれば? .text-sm
クラスを使ってください。
プロジェクトの全員が限られたオプションの中からスタイルを選択すると、プロジェクトのサイズに合わせてCSSが直線的に増大してしまうことを防ぎ、タダで一貫性が得られます。
それでもコンポーネントは作成すべき
一部の機能本位のCSS擁護派と私とで少し意見が異なる点のひとつは、ユーティリティだけでなにかを構築すべきではないと思うということです。
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
:ホバー時に明るい紫の背景を使用します
同じクラスの組み合わせで複数のボタンが必要な場合は、CSSではなくテンプレートで抽象化するのがTachyonsで推奨されている方法です。
たとえば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コンポーネントを作成した方が実用的なユースケースが多いと私は今でも思っています。
私が取り組んでいるようなプロジェクトでは、サイト上のすべての小さなウィジェットをテンプレート化するよりも、7つのユーティリティを束ねた.btn-purple
クラスを新たに作成した方が普通はシンプルです。
ですが、最初はユーティリティを使って構築します。
私が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ではメディアクエリごとのクラスが提供されています)
どこから始めるか
このアプローチが興味深く思えるなら、注目すべき価値のあるフレームワークをいくつか紹介します:
最近では私がTailwind CSSというフリーのオープンソースPostCSSフレームワークもリリースしていますが、これはユーティリティファーストで作業し、繰り返されるパターンからコンポーネントを抽出するという考え方にもとづいて設計されています:
興味のある方はTailwind CSSのウェブサイトにアクセスして試してみてください。