この記事はCSS Utility Classes and "Separation of Concerns"の翻訳です。(作者から翻訳許可を頂いています)
この記事はtailwindcssの作者であるAdam Wathanが、何故tailwindcssのようなutility-firstなCSS frameworkを作成するに至ったのかを解説したものです。
注: 訳者は今回初めて英文記事を翻訳をしました。翻訳調の読みづらい文章になっていると思うので理解しづらい部分や間違っている部分があればコメントで教えてください。修正いたします。
CSSユーティリティクラスと「関心の分離(Separation of Concerns)」
ここ数年で、私のCSSの書き方は、一般的な「Semantic」(構造的)なアプローチから、「Functional CSS」(関数型CSS)と呼ばれるスタイルへと移行しました。
Functional CSSの書きかたは多くの開発者にとって生理的な嫌悪感を呼び起こすものです。この記事では私が何故このやり方へと辿り着いたかを説明し、その過程で学んだいくつかの知見と教訓を共有したいと思います。
Phase 1: "Semantic" CSS
CSSの学習する際によく聞くベストプラクティスの1つとして「関心の分離(Separation of Concerns)」があります。
HTMLはコンテンツについての情報だけを含むべきであり、スタイル(見た目)の決定はすべてCSSでなされるべきだという考え方です。
このHTMLを見てください。
<p class="text-center">
Hello there!
</p>
.text-center
クラスが見えますか?デザイン上の決定である、スタイルの設定情報のテキスト中央揃えをHTMLに流し込んでいるため、このコードは「関心の分離」に違反しています。
代わりに推奨される方法として、HTML要素のコンテンツに基づいて要素にクラス名を付け、それらのクラスをCSSのフックとして使用してマークアップをスタイルする方法があります。
<style>
.greeting {
text-align: center;
}
</style>
<p class="greeting">
Hello there!
</p>
このアプローチの典型的な例としてCSS Zen Gardenがあります。このサイトは「関心の分離」によって、スタイルシートを交換するだけでサイトを完全に違うデザインへと変更できます。
ワークフローはこうなります:
- 新しい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>
- コンテンツに基づいた名前のクラスを追加します。
- <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>
- 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はマークアップ構造にとても深い影響を受けていました。
結局のところ関心の分離は明確に出来ていなかったのです。
Phase 2:構造から見た目を分離する
この密結合に対する解決策を探した結果、私はマークアップにクラスをさらに追加して直接CSSのターゲットにする方法を好むようになりました。これによりセレクタの特定性を低く保て、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;
}
これは私にとって大きな改善のように感じました。マークアップはまだ「Semantic」であり、見た目の決定もそれに含まれていませんでした。そして今、私のCSSは私のマークアップ構造から切り離されているように感じました。
しかし、それでもまた私はジレンマに遭遇します。
似たようなコンポーネントの扱い
サイトに新しい機能を追加する必要があるとします。カードレイアウトで記事のプレビューを表示する機能です。
この記事のプレビューカードは上部にフチなしの画像、下にパディングされたコンテンツのセクション、太字のタイトル、さらに何らかの小文字の本文があるとします。
結果的に作者の略歴とほぼ同じ見た目になりました。
関心の分離を維持しながらも、この問題に対応するベストな方法は何でしょうか?
.author-bio
クラスを記事のプレビューに適用すれば「Semantic」では無くなります。なので我々は.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:スタイルを複製する
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
もう1つのアプローチは、CSSプリプロセッサの機能の@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の重複を取り除きました、そしてマークアップはまだスタイルの決定から自由です。
さらにもう1つオプションを検討しましょう。
オプション3:コンテンツにとらわれないコンポーネントを作成する
.author-bio
と.article-preview
コンポーネントは、「Semantic」の観点からは何も共通点がありません。1つは作者の略歴、もう1つは記事のプレビューです。
しかし、すでに見たように、このふたつのコンポーネントはデザインの観点から多くの共通点があります。
なのでそれらの共通要素を元に名付けた新しいコンポーネントを作成すれば、コンテンツごとに2種類のコンポーネントに分けずともこのコンポーネントを再利用出来ます。
このコンポーネントを.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ごと編集しなければなりません。なんたることか!←(Blasphemy(冒涜)の訳し方わからん)
しかし、違う側面から見てみてください。
(But let's think about the flip side for a minute.)
↑訳すの難しい。。。
また同じスタイルの新種類のコンテンツを追加する場合はどうなるでしょう?
「Semantic」なアプローチの場合、新しい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は独立していません。HTMLはCSSによって提供されたクラスを利用しており、HTMLはどんなクラスが存在するのかを知る必要があります。どんな場合でもこれらのクラスを組み合わせることにより望んだデザインを実現します。
このやり方では、CSSは再利用可能ですが、HTMLはスタイル変更出来ません。
CSS Zen Gardenが1番目のアプローチをとる一方で、BootstrapやBulmaなどのUIフレームワークは2番目のアプローチをとっています。
どちらも本質的に「間違っている」わけではありません。特定の状況下にて、あなたにとって何がより重要かによって下された決断です。
あなたが取り組んでいるプロジェクトに必要なのはどちらですか?再スタイル可能なHTML、あるいは再利用可能なCSSでしょうか?
再利用性の選択
Nicolas GallagherのAbout HTML semantics and front-end architectureを読んだことが私にとっての転機になりました。
ここで繰り返し彼の論点全ての説明はしないですが、この記事によって私が取り組むような類いのプロジェクトにおいては再利用可能なCSSへの最適化が正しい選択であると私は確信しました。
Phase 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
クラスを使用するのは理にかなっていません。何故ならばこのボタンはスタックフォームの一部ではないからです。
しかしこれらのボタンは両方ともそれぞれのページの主要(primary)なアクションです。ならば、先頭の.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>
次にこのスタックフォームが影でふわりと浮いて見えるカード(floated card)の中にあるように見せたいとしましょう。
1つめのアプローチは、Modifier Class(修飾子クラス)を作成してそれをこのスタックフォームに適用することです。
- <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を書く必要もありません。
サブコンポーネントを作るよりもコンポーネントを組み合せるの方が優れている(Composition over subcomponents)
スタックフォームの一番下に別のボタンを追加する必要があり、既存のボタンから少し離して配置したいとします。
<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>
1つめのアプローチは、.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
コンポーネントを作るために払わねばなりません。
これは最初にコンテンツに基づいてHTML要素にクラス名を付けていた時に発生した問題とよく似ていませんか?
この問題はより再利用が簡単な全く新しいコンポーネントを作り、それを組み合わせて構成することで解決します。
おそらく.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
のうちの1つを左寄せにしたく、もう一方が右寄せにしたい時にはどうするのでしょうか?.actions-list--left
と.actions-list--right
Modifiersクラスを作るのでしょうか?
Phase 4:コンテンツにとらわれないコンポーネント+ユーティリティクラス
このようにコンポーネント名を考え続けるのは大変です。
.actions-list--left
のようなModifiersクラスを作るとき、あなたはたった一つのCSSプロパティを割り当てるために全く新しいModifiersコンポーネントを作成しています。コンポーネント名の中にleft
が入っているので、もはや誰も「Semantic」であるとは騙せないでしょう。
左揃えと右揃えのModifiersクラスを必要とする別のコンポーネントがある場合、また新しいModifiersコンポーネントを作るのでしょうか?
これは私たちが.stacked-form__footer
と.header-bar__actions
の使用を辞め、ただ1つの.actions-list
に置き換えた際に出会った問題と同じです。
複製するよりもパーツを組み合わせた方が望ましいのです。
もし2つのactions lists
があり、1つは左寄せにする必要が、もう1つは右寄せにする必要がある場合、どのように小さなパーツの組み合わせでこの問題を解決するのでしょうか?
Alignment utilities
この問題を小さなパーツの組み合わせで解決するには、コンポーネントに新しい再利用可能なクラスを追加して、目的の効果が得られるようにする必要があります。
私たちはすでにmodifersクラスを.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に「左」と「右」という言葉が表示されていて生理的な気持ち悪さを感じる場合は、これまで私たちはUIの視覚パターンにちなんで名付けられたコンポーネントを使用してきたことを思い出してください。
もはや.stacked-form
が.align-right
より「Semantec」であるとは見せかけていません。どちらもマークアップの見た目に与える影響に基づいて命名されており、狙った見た目を実現するためにマークアップでこれらのクラスを使用しています。
私たちは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
コンポーネントは削除してしまっています。
Spacer utilities
もしもスタイリングが必要なのが子要素だけならば、手の込んだ擬似セレクタを使って子要素をグループとしてスタイリングするよりも、子要素を個別にスタイリングする方がよっぽどシンプルではないでしょうか?
『要素の横にスペースを足す』という機能が名前を見て理解出来るクラスならば再利用しやすいでしょう。
私達は既に.align-left
と.align-right
を追加していますが、右にマージンを足すだけの新しいutilityクラスを追加するのはどうでしょうか?
さっそく要素の右側に少量のマージンを追加する.mar-r-sm
を新しいutilityクラスとして作成しましょう。
- .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はより小さく、クラスはより再利用しやすくなりました。
Phase 5: Utility-first CSS(汎用クラスCSS)
ひとたびこの手法が理にかなっていると思うやいなや、すぐに私は微調整用の共通ユーティリティクラスセットを作り始めました。このようなクラス群です。
- テキストのサイズ、色、太さ
- 枠線の色、幅、および位置
- 背景色
- Flexboxユーティリティ
- パディングヘルパーとマージンヘルパー
驚くべきことに、このアプローチでは新しい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コンポーネントによって作りたいとします。このCSSコンポーネントを何と呼ぶべきでしょう?
コンテンツ固有の名前は使いたくありません。そうすればこのコンポーネントは1つのコンテキストでしか使用できなくなります。
おそらくこうなるでしょう
.image-card-with-a-full-width-section-and-a-split-section { ... }
もちろんこんな馬鹿げたことはしません。その代わりに、これまで説明してきたように小さなコンポーネントから組み合わせて作るべきです。
その組み合わせる小さなコンポーネントはどんなものになるでしょうか?
そのコンポーネントは、おそらくカードの中に収まるでしょう。いくつかのカードには影を付けたいでしょうから、そのためにmodifierクラスとして.card--shadowed
を作るか、utilityクラスとしてどんな要素にも使えるように.shadow
を作成します。再利用しやすそうなutilityクラスにしましょう。
今度はいくつかのカードに角丸をつけるために.card--rounded
が必要だと判明しました。しかしサイト内の他のカードでない要素にも同じ角丸を付けたくなることもあるでしょうから、rounded
utilityクラスの方が再利用可能しやすそうです。
カード内上部の画像はどうでしょうか?カード上部いっぱいを満たすので.img--fitted
と名付けるべきでしょうか?どうやらサイト内で他にも親要素の横幅いっぱいまで満たす要素がありそうです。しかも常に画像とは限りません。単に.fit
と名付けるべきでしょう。
もうお分かりになったでしょうか?
ここまでの例から分かるように、再利用可能性に重点を置いた場合、コンポーネントを再利用可能なutilityクラスを組み合わせて構築するのは自然な結果なのです。
強化された一貫性
小さくて使いやすいutilityクラスを使用する最大の利点の1つとして、チームの開発者全員が常に決められたオプションの中から値を選択することがあります。
今まで何度HTMLをスタイルする際に、「このテキストはもう少し暗くする必要がある」と考え、$text-color
の色を調整する為にdarken()
関数を使いましたか?
それとも、今まで何度「このフォントはもう少し小さくするべきだ」とfont-size: .85em
をコンポーネントに追加しましたか?
ランダムな値でなく、ベースの色やフォントサイズから相対的な色や相対的なフォントサイズを使用しているため、「正しい」ことをしているように感じます。
しかし、もしもあなたがコンポーネントのテキストを10%暗くした際に、チームの他のメンバーがコンポーネントのテキストを12%暗くしたらどうなりますか?最終的にこのプロジェクトのテキストカラーの種類はとんでもない数になるでしょう。
これは新しいCSSを書くしかスタイルを変更する手法がない限り、すべてのコードベースで発生してしまいます。
実際の
- GitLab:402のテキストの色、239の背景色、59のフォントサイズ
- バッファ:124のテキスト色、86の背景色、54のフォントサイズ
- HelpScout:198のテキスト色、133の背景色、67のフォントサイズ
- Gumroad:91色のテキスト、28色の背景色、48サイズのフォント
- Stripe:189のテキスト色、90の背景色、35のフォントサイズ
- GitHub:163のテキストの色、147の背景色、56のフォントサイズ
- ConvertKit:128のテキスト色、124の背景色、70のフォントサイズ
何故ならばあなたが書くCSSはすべて空白のキャンバスだからです。あなたは好きな値を何でも使え、何も制限はありません。
変数やミックスインを使うことで一貫性を強化は出来ますが、それでも新しく書かれたCSS全てが新しい複雑さの原因になります。CSSを追加し続ける限りCSSがシンプルになることは決してありません。
その代わりに、スタイリングをする際に新しいクラスを作るのではなく既存のクラスを適用するようにすれば、ぱっと空白のキャンバス問題はなくなります。
暗いテキストを少し柔らかい色にしたいですか?.text-dark-soft
クラスを追加してください。
フォントサイズをもう少し小さくしたいですか?.text-sm
クラスを使用してください。
プロジェクトの参加者全員が限られた既に選ばれた選択肢から選択してスタイルを決めると、CSSはプロジェクトのサイズに比例して大きくなるのをやめ、労せずに一貫性を保てるようになります。
それでもComponentは必要になる
私の意見は熱心なFunctional CSSの信奉者とは少しだけ違います。utilityクラスのみでスタイルを構築すべきだとは思っていません。
Tachyons(素晴らしいプロジェクトです)のような一般的なユーティリティベースのフレームワークをいくつか見てみると、ボタンのスタイルすら純粋なUtilities Classから作成していることがわかります。
<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple">
Button Text
</button>
早速これを分解してみましょう。
- f6:フォントサイズスケールの6番目のフォントサイズ(Tachyonsでは.875rem)を適用
- br3:ラジウススケールの3番目のボーダーラジウスを適用(0.5rem)
- ph3:横方向のパディングのパディングスケールの3番目のサイズを適用(1rem)
- pv2:垂直方向のパディング(.5rem)にはパディングスケールの2番目のサイズを適用
- 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コンポーネントの方が実用的な場面もたくさんあります。
私が手掛けるような種類のプロジェクトでは、いちいちサイト上の小さなウィジェットを全てテンプレート化していくよりも、そのテンプレートで使われた7つのユーティリティクラスを一つにまとめた.btn-purple
のような新しいクラスを作成する方が殆どの場合簡単です。
...しかし、最初にユーティリティを使ってそれらをビルドする
ここで取っているアプローチを私がCSS utility-firstと呼ぶ理由は、基本的にutilityクラスから全て構築をして、繰り返し出現するパターンのみを抽出してCSSコンポーネントにしているからです。
もしLessをプリプロセッサとして使用している場合には、すでに存在しているクラスをミックスインとして使用出来ます。つまりマルチカーソルのちょっとした小ワザを使えば.btn-purple
コンポーネントを簡単に作れるのです。
残念ながら、SassやStylusでは、ユーティリティクラスごとに別々のミックスインを作成しなければならず、もう少し手間がかかります。
もちろん常にコンポーネント内のスタイル全てをutilityクラスから作れるわけではありません。親要素の上にマウスを移動したときに子要素のプロパティを変更するような要素間の複雑なインタラクションは、utilityクラスのみでは実現困難です。したがって、どうすればシンプルに書けるだろうかとあなたの基準に照らし合わせながら判断して下さい。
早すぎる抽象化はやめよう
CSSに対してcomponent-firstのアプローチをとるということは、たとえ二度と再利用されることが決してないようなものでさえコンポーネントになるということです。この早すぎる抽象化は複雑で膨大なスタイルシートを作る原因となります。
例えばナビゲーションバーを作るとします。メインナビゲーションのマークアップをアプリ内で何回書き変えるでしょうか?
私のプロジェクトでは殆どの場合メインのレイアウトファイルに一度しか書きません。
utility-firstで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を作成するときに直面するのと同じ空白のキャンバス問題が起こります。
utility-firstでは選択を強います。
これはtext-sm
かtext-xs
か?
py-3
またはpy-4
を使うべきか?
text-dark-soft
かtext-dark-faint
どちらを使うべきか?
自由に値を選ぶことはできません。既に選ばれたリストから選択する必要があります。
380色のテキストカラーの代わりに、10色または12色のテキストカラーになります。
utility-firstのアプローチは私の経験上、直感に反してcomponent-firstのアプローチよりも一貫性のあるデザインを構築しやすいです。
早速試してみたい
このアプローチに興味がある方のために、ここでチェックする価値のあるいくつかのフレームワークを紹介します。
私は最近、Tailwind CSSという無料のオープンソースPostCSSフレームワークをリリースしました。utility-firstと何度も繰り返すパターンのためのコンポーネントの抽出というアイデアに基づいて設計されたフレームワークです。
もし興味があれば、Tailwind CSSのWebサイトにアクセスして試してみてください。