EmotionとSvelteを組み合わせる場合の設定とうまくいかない部分とか

CSS in JSライブラリのEmotionにはReact版とFramework agnostic版がある。なのでReact以外のビューライブラリと組み合わせたりバニラJSでも使ったりできるけど、Babelプラグインが実質必須なのでSingle File Componentsみたいなスタイルとは相性が悪い。

<script>
  import { css } from "emotion";
</script>

<h1 class={css({ color: "pink" })}>Hello!</h1>

EmotionのBabelプラグインはコードの最適化に加えてCSSのソースマップ生成を行う。Emotionが生成するクラス名はランダムなハッシュ値になって可読性がないので、デバッグするにはソースマップがかなり重要になる。Babelプラグインが正しく機能すれば開発者ツールで検出した宣言ブロックからソースコード内の定義元が参照できるようになる。

出典:Emotion - Source Maps

一方でSvelteコンポーネントはそのままではBabelでパースできないので、このBabelプラグインを使うには次のいずれかの方法を取るしかない:

  1. svelte-loaderあるいはrollup-plugin-svelteによってJavaScriptに変換された後にBabelを適用する
  2. Svelte Preprocessによってscript要素内のみにBabelを適用する

1の場合、JavaScriptコンパイルされた後のソースコードを基準にしてソースマップを生成するので、ユーザーが実際に編集するファイルと別のものを参照してしまう。

2の場合、script要素内のコードについては正しくソースマップが生成されるが、テンプレートにはBabelが適用されないので、テンプレートに直接記述した定義にはソースマップが生成されない。この場合テンプレート内の最適化も行われない。

またいずれにしてもSvelteコンポーネントの外でスタイルを定義してimportすればソースマップは正しく生成される。

import { css } from "emotion";

export const hello = css({ color: "pink" });
<script>
  import { hello } from "./styles";
</script>

<h1 class={hello}>Hello!</h1>

しかしこの使い方だとインラインスタイル風に書きたいというモチベーションが満たせないので自分としてはあまり意味がない。同じ理由でscript要素内にすべて定義してしまうのも無い。

そのためどちらかというと1の方がマシという結論。ソースマップがまったく無いよりは良いし、最適化も漏れなく適用させたい。webpack.config.jsはこんな感じ:

module: {
  rules: [
    ...
    {
      test: /\.svelte$/,
      use: [
        {
          loader: "babel-loader",
          options: {
            plugins: ["emotion"],
          },
        },
        {
          loader: "svelte-loader",
        },
      ],
    },
    ...
  ],
},

またVS Codeでは、Babel JavaScriptvscode-styled-componentsをインストールすることでTemplate literals内に記述したCSSシンタックスハイライトされたり補完が効くようになったりするが、Svelteコンポーネントのテンプレート中ではそれが有効にならない。script要素内では動く。

代わりにObject Stylesで記述すると特別なプラグインを足さずともまともに書けるようになるし、Emotionの型定義ファイルからプロパティ名や値を補完してくれる。最低限Svelte for VS Codeは必要。

次世代インラインスタイル

ユーティリティファーストCSSは実質的にインラインスタイルであり、本来はインラインスタイル的な記述ができる方がより望ましいように思えるが、おもに次のような制約によりユーティリティファーストとして体を成している:

  • インラインスタイルでは、疑似クラス・疑似要素・子孫セレクタ@mediaをはじめとする@-規則などが利用できない
  • インラインスタイルは多くのエディタでうまく補完されない
  • デザイントークンを参照できない(あるいはしづらい)

一方でいくつかのCSS in JSライブラリは別のアプローチを選択することでこれらの制約を回避しており、また比較的新しいCSSネイティブの機能の利用によっても解決できるようになってきた。ユーティリティファーストを完全に置き換えられるわけではないにしても、それらの代替案を検討できる場面もあるだろう。

CSS in JSのアプローチ

たとえばEmotionでは、通常のCSSファイル内と同じように宣言ブロックの中身をcss関数の引数として記述することで対応するクラス名が生成されるAPIになっている:

import { css, cx } from 'emotion'

const color = 'white'

render(
  <div
    className={css`
      padding: 32px;
      background-color: hotpink;
      font-size: 24px;
      border-radius: 4px;
      &:hover {
        color: ${color};
      }
    `}
  >
    Hover to change color.
  </div>
)

Sassのように&によって疑似クラスを表現できて、JavaScriptのテンプレートリテラルなのでスコープ内の変数も参照できる。メディアクエリも書ける。いわばフルスペックなインラインスタイルだ。

styled-componentsにあるcss propを使っても同じようなことができる。

<div
  css={`
    background: papayawhip;
    color: ${props => props.theme.colors.text};
  `}
/>

この記述はBabelプラグインによって次のように変換される:

import styled from 'styled-components';

const StyledDiv = styled.div`
  background: papayawhip;
  color: ${props => props.theme.colors.text};
`

<StyledDiv />

styled関数に渡したスタイル宣言から自動的にstyle要素が生成されてページに挿入される。

Emotionやstyled-componentsはランタイムとして実行されるためパフォーマンス上のオーバーヘッドがあるが、スタイル宣言をプリコンパイルしてランタイムコストなしで利用できるLinariaというライブラリもある。APIは基本的にEmotionやstyled-componentsと変わらないが、プリコンパイル時に評価しきれない表現を使えないトレードオフはある(ある程度はJavaScriptを評価してくれる)。

import { css } from 'linaria';
import { modularScale, hiDPI } from 'polished';
import fonts from './fonts';

<h1
  className={css`
    text-transform: uppercase;
    font-family: ${fonts.heading};
    font-size: ${modularScale(2)};

    ${hiDPI(1.5)} {
      font-size: ${modularScale(2.5)};
    }
  `}
>
  Hello world
</h1>

難点として、この手のライブラリにつきものなのがデバッグの煩わしさであり、クラス名はハッシュ値として生成されるため可読性がなく、ソースマップもサポートされていないような場合がある。

styled-componentsではクラス名に、コンポーネントと対応するハッシュ値に加えてコンポーネントに紐づいている変数名をもとにしたApp___StyledDiv-mo47nu-0のような値を付与しているが、ソースマップは今のところサポートされていない。EmotionとLinariaではクラス名はハッシュ値のままになるがソースマップがサポートされている。

これらのためのシンタックスハイライトや補完は、主要なエディタにはプラグインとしてコミュニティによって提供されている。構文はどれもstyled-componentsと変わらないので同一のプラグインで用が足りる。

しかしそれでもこのような周辺ツールの開発にはそれなりのリソースが費やされており、stylelintとの統合なども含めて、独自性の高いアプローチを実現するためには膨大な労力が必要になってしまう。このような問題に対してSvelteは、もとあるものをできるだけそのままにした「十分な」やり方を提供することをあえて選択している。

カスタムプロパティによる表現力の拡張

インラインスタイルでは疑似クラスや疑似要素などを直接宣言することはできないが、カスタムプロパティを利用すれば間接的にそれが実現できる。カスタムプロパティの値はカスケードされるため、インラインスタイルからカスケードされ得るあらゆる宣言はすべてカスタムプロパティとしてインラインスタイルから挿入できる。

たとえば:hoverに対応するcolorプロパティは次のように表現できる:

a {
  color: var(--color);
}

a:hover {
  color: var(--hover-color);
}
<a
  href="/hello"
  style="
    --color: dodgerblue;
    --hover-color: mediumblue;
  "
>
  Hello
</a>

このように「インラインスタイルで表現できない宣言」の値を「インラインスタイルから挿入できる仕組み」を実装しておくと、本来インラインスタイルではできなかったはずのスタイリングが実現可能になる。すべての要素に対してこの手法を適用すると、ユーティリティーファーストCSSと同じようにほぼセレクタを書かずに開発していけるようになる。たとえば次のようにすると、メディアクエリごとのdisplayプロパティの値がインラインスタイルで指定できるようになる:

* {
  --display: initial;
  display: var(--display, revert);

  @media (min-width: 45em) {
    --md--display: initial;
    display: var(--md--display, var(--display, revert));
  }

  @media (min-width: 60em) {
    --lg--display: initial;
    display: var(--lg--display, var(--md--display, var(--display, revert)));
  }
}
<div
  style="
    --display: none;
    --md--display: block;
    --lg--display: inline-block;
  "
>
  Hello
</div>

通常カスタムプロパティは継承されるが、initialキーワードを指定すると継承されなくなる。これはguaranteed-invalid valueと呼ばれるカスタムプロパティに固有の仕様である。その上で指定しているrevertキーワード(実装はまだ十分ではない)は、値をユーザーエージェントスタイルシートのデフォルトスタイルにフォールバックする役割がある。これによって、インラインスタイルが指定されている場合にはその値が利用されて、指定がない場合はデフォルトスタイルのままになる挙動が実装できる。

しかしエディタでの入力はあまり快適ではない。利用されるカスタムプロパティの解析しづらさも含めて改善は難しそうに思える。あるいは.tsxファイルではなんとかなる可能性があるかもしれない。

デザイントークンの管理と適用

Sassの変数として管理されているようなカラーコードや余白のサイズなど、スタイル上で利用する値のセットをデザインシステムの文脈ではデザイントークンと呼ぶ。従来CSSには変数の機能がなかったのでデザイントークンの管理にはSassなどのツールが必要とされていたが、IEの後の世界にはCSSネイティブの機能としてカスタムプロパティがあるので単にそれを利用すれば良い。

:root {
  --color-blue: hsl(240, 100%, 27%);
}
<div style="color: var(--color-blue);">Hello</div>

ユーティリティファーストCSSではその性質上、あらかじめデザイントークンが決まっていなければユーティリティファーストな開発を行えない。インブラウザデザインのような制作方法であればそうした実装上の細かい制約を意識しながら作っていけるかもしれないが、そうでなくデザインファイルなどをもとにして実装していく場合では最初に値だけを予測して設定するのはかなり難しい。

CSSでは、最初は値がハードコーディングされた状態から始まり、必要に応じて後から共通化していくのが現実的。これは値だけに限らず、そもそもユーティリティファーストCSSというアプローチ自体が「決定を遅延させる」考え方だと言える。しかしながら、ユーティリティファーストなアプローチを取るためにはこのデザイントークンだけは先に決まっていなければならないジレンマがある。このあらかじめの設定を意味のある制約だと言う人もいるが、少なくとも「決定を遅延させる」指向とで議論を分けるべきだろう。

翻訳:CSSユーティリティクラスと「関心の分離」(いかにしてユーティリティファーストにたどり着いたか)

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;
}

View on CodePen

これは大きな改善のように感じられました。マークアップはまだ「セマンティック」であり、スタイリングの決定を含んでいませんでしたが、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;
}

View on CodePen

@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通りあります:

  1. 「関心の分離」
    HTMLに依存するCSS

    コンテンツにもとづいたクラス名をつけることで(.author-bioのように)、HTMLをCSSの依存関係として扱います。

    HTMLは独立しています。HTMLがどのように見えるかは気にせず、HTMLが統制する.author-bioのようなフックを公開しているだけです。

    一方でCSSは独立したものではありません。HTMLが公開すると決めたクラスを知る必要があり、HTMLのスタイルを整えるためにそれらのクラスを標的にする必要があります。

    このモデルではHTMLは再スタイリング可能になりますが、CSSは再利用できません。

  2. 「関心の混合」
    CSSに依存するHTML。

    UIのパターンを繰り返した後に、コンテンツに依存しない方法でクラス名をつけると(.media-cardのように)、CSSはHTMLの依存関係として扱われます。

    CSSは独立しています。どのようなコンテンツに適用されるかの関心は持たず、マークアップに適用できる構成要素のセットを公開しているだけです。

    HTMLは独立していません。CSSによって提供されているクラスを利用しており、その組み合わせによって望ましいデザインを達成するためには、どのようなクラスが存在するかを知る必要があります。

    このモデルではCSSは再利用可能になりますが、HTMLは再スタイリングできません。

CSS Zen Gardenは最初のアプローチを取り、BootstrapBulmaのような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-smtext-xsか?

使うべきはpy-3py-4か?

使いたいのはtext-dark-softtext-dark-faintか?

好きな値を選ぶことはできず、キュレーションされたリストから選択しなければなりません。

文字色は380色ではなく、結局10色か12色になります。

私の経験では、コンポーネントファーストで作業するよりもユーティリティファーストで構築することで、より一貫した外観のデザインが得られると考えています。

(訳注:クラス化によってメディアクエリを利用できるという利点もあり、実際にTailwind CSSではメディアクエリごとのクラスが提供されています

どこから始めるか

このアプローチが興味深く思えるなら、注目すべき価値のあるフレームワークをいくつか紹介します:

最近では私がTailwind CSSというフリーのオープンソースPostCSSフレームワークもリリースしていますが、これはユーティリティファーストで作業し、繰り返されるパターンからコンポーネントを抽出するという考え方にもとづいて設計されています:

興味のある方はTailwind CSSのウェブサイトにアクセスして試してみてください。

実践的レイアウトプリミティブ

CSSにおける汎用化の先送り、ユーティリティファーストCSS、レイアウトプリミティブ」の続き。



同じようなレイアウトを実現するためのCSSを僕は実のところ何度も繰り返し書いていた。そのたびに新しいコンポーネントを作り、意図を表明するための名前を捻り出し、やってることはたいして変わらないのに別々になった実装を増やしていた。その総量に埋もれて全体が見えなくなっていった。

個別のコンポーネントを汎用的なように変換するのは難しい。本当にまったく同じレイアウトならそれほど難しくないが、多くの場合には微妙な差分がある。余白の大きさが違う、グリッドのカラム数が違う、コンテナの幅が違う。いかにしてそれらに規則性を見い出してうまくいく設計ができるかは、腕の見せどころとも言える一方で再現性がなく見通しのつかない仕事だと思っていた。

Every Layoutのレイアウトプリミティブはそのようなレイアウトの構成要素が最小単位まで分解され、パターン集として文書化されたもの。これら最小単位のパターンはパターンと対応するコンポーネントとして切り出せる粒度になっていて、また複数のパターン同士を組み合わせることによって最終的なレイアウトを実現する前提で設計されている。有名なところではBootstrapのグリッドシステムの設計が近い:

<div class="container">
  <div class="row">
    <div class="col-sm">
      One of three columns
    </div>
    <div class="col-sm">
      One of three columns
    </div>
    <div class="col-sm">
      One of three columns
    </div>
  </div>
</div>

出典:Grid system · Bootstrap v4.5

.rowは子要素をカラムとして扱うためのコンテナであり、.col-smはカラムである子要素につねに対応するので、カラムのパターンとしては.row.col-smはセットになるが、.containerはコンテナ幅を制御するためだけのクラスであるためカラムの実現には必要ではない。.container.rowは互いに依存関係を持たない独立した存在であり、それぞれは自らの責務のみを意識している。そしてテンプレート側でこれら別々のパターンを組み合わせることで、最終的な結果として、制御されたコンテナ幅の中でカラム分割されたレイアウトが実現される。

このように独立したパターンを組み合わせてレイアウトする利点は、あるパターンの利用が別のパターンに制約されなくなることにより組み合わせ可能なバリエーションが大幅に増え、より少ないCSSでより多くのレイアウトを実現できるようになる冗長性にある。あるパターンに3のバリエーションがあり、別のパターンには5のバリエーションがあるとき、テンプレート側でそれらをかけ合わせると15のバリエーションを表現できるようになる。CSSは8のままで。

レイアウトプリミティブを利用するとそれだけで相当数のレイアウトが表現できる。すべてのパターンがプリミティブであり、かつそれぞれが組み合わせ可能なことを前提としているからだ。ひとつひとつのパターンとしても利用頻度が高いものが多く、自分が携わるほとんどのサイトの構築において汎用的なコンポーネント設計のパターンとして効果を上げた実績もある。

しかしEvery Layoutでの解説にもとづくとそのままでは現実のプロジェクトに適用しづらい部分がある。そのひとつはパターンのバリエーションをカスタムプロパティを用いて表現していること。多くのプロジェクトではIE11でもほとんど同じように表示できることを求められるのでこれは採用できない。ポリフィルもあまり信用できないし。もうひとつはメディアクエリによる上書きを意図的に想定していないこと。Every Layoutはレイアウトの制御をブラウザや公理に委ねることを強く主張しており、ビューポート幅にもとづくスタイル宣言の変更というある種恣意的なレイアウトの操作に依存しないようにレイアウトプリミティブも設計されている。しかしそれでは少なくとも業務での実践は難しいだろう。

この記事ではレイアウトプリミティブを現実のプロジェクトに取り入れるために行ったいくつかの対処方法を紹介する。レイアウトプリミティブをコンポーネントとして実装していく方向で進めるが、後述する理由によりすべてをコンポーネントにはしない(あるいはできない)。なおこの記事ではそれぞれのレイアウトプリミティブについては詳しく言及せず、あらかじめ理解されている前提で述べる。必要に応じてEvery Layoutをご参照いただきたい。また記事中では説明の都合上、ソースコードの一部のみを抜粋して掲載している。完全な状態は記事の末尾でまとめて確認できる。

バリエーション

Stackは縦方向に配置された要素間に均一の余白を設定するパターン。この場合の余白の大きさの指定方法として次のような実装が紹介されている。

<div class="stack">
  <p>Lorem ipsum dolor sit amet.</p>
  <p>Lorem ipsum dolor sit amet.</p>
  <h2>title</h2>
  <p>Lorem ipsum dolor sit amet.</p>
  <p>Lorem ipsum dolor sit amet.</p>
</div>
.stack {
  --space: 1.5rem;
}

.stack > * + * {
  margin-top: var(--space);
}

h2,
h2 + * {
  --space: 3rem;
}

.stack--spaceプロパティの上書きによってデフォルトの余白が変更可能になっており、個別の要素の前後の余白は--spaceプロパティの宣言によって、詳細度の高い.stack > * + *margin-topを上書きせずに変更できる。

--spaceプロパティのおかげで.stackは自らが表現する余白のバリエーションを知っておかなくてもよくなる。場面に応じて利用する側から指定されればそれに対応できる。これがカスタムプロパティを使えないとすれば、BEMのモディファイアのようなやり方で余白の大きさを指定することになる。モディファイアでそのままサイズを指定することはできないので、キーであるモディファイアと値との対応を考えなければならない。つまりあらゆる余白のバリエーションを把握する必要がある。

デザインガイドラインとして余白のバリエーションが文書化されていればそれを利用できるが、多くの場合では各ページのデザインファイルから地道に拾い上げていくしかない。しかしそうするとバリエーションが膨大になってしまったり、デザイン変更のたびにバリエーションが影響を受けて再考の作業が生まれてしまう。多くの箇所で再利用する前提のコンポーネントに参照されている以上、余白のバリエーションは可能な限り早い段階でフリーズさせたい。最初からあらかじめわかっている状態にできればベストだ。そのため特定のページだけに依存せずあらゆるプロジェクトに適用できるパターンを模索してきたが、現在としては「音楽、数学、タイポグラフィ」で紹介された8px(0.5rem)を基数としてフィボナッチ数列をかけ合わせて生成されたバリエーションをベースに少し変形させたものを足がかり的に利用している。その状態から最後まで変更なしのままプロジェクトを見届けることもあれば、細かい調整が必要になることもあるが、まったく見当違いの設定になっていることはなかった。

こうして計画した余白のバリエーションから、Sassを利用してモディファイアを次のように実装している。

_core.scss:

// Spacing

$-spacing-unit: 0.5rem;

$spacing-1: $-spacing-unit / 2;
$spacing-2: $-spacing-unit * 1;
$spacing-3: $-spacing-unit * 1.5;
$spacing-4: $-spacing-unit * 2;
$spacing-5: $-spacing-unit * 3;
$spacing-6: $-spacing-unit * 5;
$spacing-7: $-spacing-unit * 8;
$spacing-8: $-spacing-unit * 13;
$spacing-9: $-spacing-unit * 21;

$spacings: (
  0: 0,
  1: $spacing-1, // 0.25rem =   4px
  2: $spacing-2, //  0.5rem =   8px
  3: $spacing-3, // 0.75rem =  12px
  4: $spacing-4, //    1rem =  16px
  5: $spacing-5, //  1.5rem =  24px
  6: $spacing-6, //  2.5rem =  40px
  7: $spacing-7, //    4rem =  64px
  8: $spacing-8, //  6.5rem = 104px
  9: $spacing-9, // 10.5rem = 168px
);

(余白のバリエーションを個別の変数とマップで宣言しているのはエディタの補完とループのためという事情。ループを用いない場面では個々の変数を参照する。)

_Stack.scss:

/**
 * Spacing variant:
 *
 * <div class="Stack -s{spacing}"></div>
 */

@each $spacing-key, $spacing in $spacings {
  $name: s#{$spacing-key};

  .Stack.-#{$name} > * + * {
    margin-top: $spacing;
  }
}
<div class="Stack -s4">
  <p>foo</p>
  <p>bar</p>
  <p>baz</p>
</div>

こうしてあらゆる縦の余白のバリエーションがひとつのコンポーネントだけで実現できるようになった。しかしこれだけでは特定の箇所に異なる余白がある場合に対応できない。それについては2つのやり方を場面によって使い分けている。ひとつはユーティリティクラスの導入だ。なんとなく導入されたユーティリティクラスは悪い設計を招いてしまうが、利用の目的がはっきりとしていれば問題はない。この場合ではStackのコンテキスト内で個別のmargin-topを設定すること。またそのユーティリティクラスを使うことで新たなセレクタを増やさず済ませられる場合にも利用できる。

_utilities.scss:

// margin-top property

/**
 * Usage:
 *
 * <div class="Stack -s2">
 *   <div>foo</div>
 *   <div class="mt-4">bar</div>
 *   <div>baz</div>
 * </div>
 *
 * <div class="mt-3"></div>
 *
 * Spacing variant:
 *
 * <div class="mt-{spacing}"></div>
 */

@each $spacing-key, $spacing in $spacings {
  $name: mt-#{$spacing-key};

  .#{$name} {
    margin-top: $spacing !important;
  }
}

margin-bottompaddingなどのユーティリティクラスは本当に必要になるタイミングまで作らない。

もうひとつのやり方は、Stackとそもそも別のコンポーネントを作ってしまうこと。新しいコンポーネントは増えてしまうが、特殊な対応は個別のコンポーネントに切り出して閉じ込めておいた方がいい場合もある。あるいはすでに存在するコンポーネントにBEMでいうエレメントとして追加するのであれば比較的気兼ねなく行えるだろう。

<div class="ArticleBody">
  <p>レイアウトプリミティブを現実のプロジェクトに取り入れるために…</p>
  <h2>バリエーション</h2>
  <p>Stackは縦方向に配置された要素間に均一の余白を…</p>
</div>

_ArticleBody.scss

.ArticleBody > * + * {
  margin-top: 1.5rem;
}

.ArticleBody > h2,
.ArticleBody > h2 + * {
  margin-top: 3rem;
}

ほとんどのレイアウトプリミティブは余白を表現する責務を持つので、余白のモディファイアについては先ほどのバリエーションを参照することで同様の解法がとれる。しかしStackの余白は縦方向のみなのに対して、たとえばClusterには縦横両方向の余白がある。Every Layoutでは縦横で同じ余白が挿入される実装になっているが、汎用性としては別々の値を指定できた方が良い。そのために全方向の余白・X軸の余白・Y軸の余白を個別に指定できる別々のモディファイアを設定するようにした:

_Cluster.scss:

.Cluster {
  display: block;
  overflow: hidden;
}

.Cluster > * {
  display: flex;
  flex-wrap: wrap;
}

/**
 * Spacing variant:
 *
 * <div class="Cluster -s{spacing}"></div>
 * <div class="Cluster -sx{spacing}"></div>
 * <div class="Cluster -sy{spacing}"></div>
 */

@each $spacing-key, $spacing in $spacings {
  $name: s#{$spacing-key};

  .Cluster.-#{$name} > * {
    margin: ($spacing / 2 * -1);
  }

  .Cluster.-#{$name} > * > * {
    margin: ($spacing / 2);
  }
}

@each $spacing-key, $spacing in $spacings {
  $name-x: sx#{$spacing-key};

  .Cluster.-#{$name-x} > * {
    margin-right: ($spacing / 2 * -1);
    margin-left: ($spacing / 2 * -1);
  }

  .Cluster.-#{$name-x} > * > * {
    margin-right: ($spacing / 2);
    margin-left: ($spacing / 2);
  }

  $name-y: sy#{$spacing-key};

  .Cluster.-#{$name-y} > * {
    margin-top: ($spacing / 2 * -1);
    margin-bottom: ($spacing / 2 * -1);
  }

  .Cluster.-#{$name-y} > * > * {
    margin-top: ($spacing / 2);
    margin-bottom: ($spacing / 2);
  }
}

またEvery Layoutではあまり言及されていない点として、ネガティブマージンによってはみ出る領域をoverflow: hiddenで非表示にしてしまうハックの扱いにくさがある。この手を使ってしまうと、ネガティブマージン以外にもはみ出るoutlineプロパティやbox-shadowプロパティ、あるいは外方向に動くアニメーションが見切れてしまう。これについては、デフォルトとしてはoverflow: hiddenを設定しておいて、問題になる箇所だけを個別にオプトアウトするようにした:

_Cluster.scss:

.Cluster {
  display: block;
}

/**
 * Overflow variant:
 *
 * <div class="Cluster -overflow"></div>
 */

.Cluster:not(.-overflow) {
  overflow: hidden;
}

.Cluster > * {
  display: flex;
  flex-wrap: wrap;
}

デフォルトをhiddenにしているのは、ネガティブマージンによるはみ出しの方がより無意識的なバグを生んでしまいそうに思えるから。

そしてClusterには余白だけでなくjustify-contentalign-itemsも個別に設定できるようになっている。オリジナルではカスタムプロパティで直接値を渡せるが、モディファイアとして表現するには余白と同様にあらかじめバリエーションを列挙する必要がある。これらのプロパティでよく使う値はだいたいわかっているのでほぼ決め打ちにできる。

_Cluster.scss

/**
 * Justify variant:
 *
 * <div class="Cluster -justify-{justify}"></div>
 */

$Cluster-justifiers: (
  start: flex-start,
  end: flex-end,
  center: center,
  between: space-between,
);

@each $justifier-key, $justifier in $Cluster-justifiers {
  $name: justify-#{$justifier-key};

  .Cluster.-#{$name} > * {
    justify-content: $justifier;
  }
}

/**
 * Align variant:
 *
 * <div class="Cluster -align-{align}"></div>
 */

$Cluster-aligners: (
  start: flex-start,
  end: flex-end,
  center: center,
  stretch: stretch,
);

@each $aligner-key, $aligner in $Cluster-aligners {
  $name: align-#{$aligner-key};

  .Cluster.-#{$name} > * {
    align-items: $aligner;
  }
}

次に、CenterはBootstrapの.containerに似たパターンで、max-widthの値はカスタムプロパティから渡される想定になっている。具体的な幅はサイトによってまちまちなので、この設定は実際のものを作り始めてみるまでどうにもならない。多くのサイトは複数のコンテンツ幅を組み合わせて構成されているので、作りながらバリエーションを探って、やはりこれもモディファイアで表現していく:

_Center.scss:

.Center {
  box-sizing: content-box;
  display: block;
  max-width: 60rem;
  margin-right: auto;
  margin-left: auto;
}

.Center.-wide {
  max-width: 75rem;
}

.Center.-narrow {
  max-width: 45rem;
}

/**
 * Gutters variant:
 *
 * <div class="Grid -noGutters"></div>
 */

.Center:not(.-noGutters) {
  padding-right: $spacing-5;
  padding-left: $spacing-5;
}

これは単純化した例で、実際にはブレイクポイントごとに個別の幅を設定することが多い。しかしブレイクポイントをまたいだときの変化が均一化されていない場合もあり、モディファイアの設計が難しいパターンかもしれない。そういった場合はStackについて述べたように別コンポーネント化してしまうか、あるいはメディアクエリ付きモディファイアの導入を検討する。

メディアクエリ

ビューポート幅が変化しても同じパターンのレイアウトのままで成立させられる場面は多いが、余白などのキーだけは個別に変更が必要になることがほとんどだ。たとえば狭いビューポート幅では余白も狭く、広いビューポート幅では余白も広くというのが典型的。結局これに対応できないと再利用性のない個別のコンポーネントを作り込んでいくしかなくなってしまう。そのためメディアクエリごとに機能するモディファイアを用意して利用側で個別に指定する形で対処した:

_core.scss:

$mq-breakpoints: (
  xs: 0,
  sm: 36em, //  576px
  md: 48em, //  768px
  lg: 64em, // 1024px
  xl: 80em, // 1280px
);

@mixin breakpoint($key, $until: false) {
  @if map.has-key($mq-breakpoints, $key) == false {
    @error "`#{$key}` not found in $mq-breakpoints";
  }
  $breakpoint: map.get($mq-breakpoints, $key);
  $is-zero: $breakpoint == 0;

  @if $is-zero and $until {
    @error "Breakpoints are not available for screens smaller than 0px";
  }

  @if $is-zero {
    @content;
  } @else if $until {
    @media not all and (min-width: #{$breakpoint}) {
      @content;
    }
  } @else {
    @media (min-width: #{$breakpoint}) {
      @content;
    }
  }
}

_Stack.scss:

/**
 * Spacing variant:
 *
 * <div class="Stack -s{spacing}"></div>
 * <div class="Stack -{breakpoint}:s{spacing}"></div>
 */

@each $breakpoint-key, $breakpoint in $mq-breakpoints {
  $uses-media-query: $breakpoint != 0;
  $breakpoint-prefix: if($uses-media-query, "#{$breakpoint-key}\\:", null);

  @include breakpoint($breakpoint-key) {
    @each $spacing-key, $spacing in $spacings {
      $name: s#{$spacing-key};

      .Stack.-#{$breakpoint-prefix}#{$name} > * + * {
        margin-top: $spacing;
      }
    }
  }
}
<div class="Stack -s3 -md:s5 -lg:s6">
  <p>foo</p>
  <p>bar</p>
  <p>baz</p>
</div>

ほかのパターンの余白やjustify-content、ユーティリティクラスなどについても同様に実装する。

メディアクエリごとの宣言を追加することでCSSの出力サイズは増えてしまうが、レイアウトプリミティブが全体の個別性を吸収することで結果的にはむしろサイズを抑えられる場合もある。あるいはレイアウトプリミティブによってCSSの実装時間を節約することで、より費用対効果の高いパフォーマンス改善に臨めるとも考えられる。

メディアクエリごとのモディファイアを記述する煩雑さについては、テンプレートエンジンの機能によってある程度軽減できる。たとえばReactであれば、次のような宣言によって上記と同様のクラス属性値が出力されるようにすると良い:

<Stack s={[3, null, 5, 6]}>
  <p>foo</p>
  <p>bar</p>
  <p>baz</p>
</Stack>

こうすれば必要に応じて型チェックも挿入できる。配列としてキーを渡すアイデアStyled SystemのArray Propsから拝借した。同様のインターフェースはPugのmixin機能などでも実現できる。

またオリジナルのレイアウトプリミティブの中には、メディアクエリには依存せずに要素自身の幅の変化によって子要素の並びが変わるSidebarやSwitchがある。これらには並びを変化させるブレイクポイントとしてそのタイミングの要素の幅をカスタムプロパティで指定するが、メディアクエリを前提とした設計であれば普通にメディアクエリで上書きした方が素直だろう。ブレイクポイントの指定は先述と同様にモディファイアで行う:

_Switcher.scss:

.Switcher > * {
  display: flex;
  flex-direction: column;
}

.Switcher > * > * {
  flex-shrink: 0;
  width: 100%;
}

/**
 * Row variant:
 *
 * <div class="Switcher -row"></div>
 * <div class="Switcher -{breakpoint}:row"></div>
 */

$Switcher-row-name: row;

@each $breakpoint-key, $breakpoint in $mq-breakpoints {
  $uses-media-query: $breakpoint != 0;
  $breakpoint-prefix: if($uses-media-query, "#{$breakpoint-key}\\:", null);

  @include breakpoint($breakpoint-key) {
    .Switcher.-#{$breakpoint-prefix}#{$Switcher-row-name} > * {
      flex-direction: row;
    }

    .Switcher.-#{$breakpoint-prefix}#{$Switcher-row-name} > * > * {
      flex-shrink: 1;
    }
  }
}
<div class="Switcher -md:row">
  <div>
    <p>foo</p>
    <p>bar</p>
    <p>baz</p>
  </div>
</div>

ビューポートの幅によってレイアウトプリミティブのパターン自体を切り替えたい場面もある。狭い幅ではカードをReel(横スクロール)で並べ、広い幅ではGridで並べるというような。個別のコンポーネントにすれば共通のマークアップで実現できなくもないが、ここは汎用性のためブレイクポイントごとに別々のマークアップを使う。JavaScriptで出し分けてもいいが、両方のパターンを含んだ静的テンプレートを記述した上で、メディアクエリごとにdisplay: noneを制御するユーティリティクラスを付与する方が簡単になる:

_utilities.scss:

// display property

/**
 * Usage:
 *
 * <div class="hidden md:block">hello</div>
 *
 * Display variant:
 *
 * <div class="{display}"></div>
 * <div class="{breakpoint}:{display}"></div>
 */

$-displayers: (
  block: block,
  inline: inline,
  hidden: none,
  inlineBlock: inline-block,
);

@each $breakpoint-key, $breakpoint in $mq-breakpoints {
  $uses-media-query: $breakpoint != 0;
  $breakpoint-prefix: if($uses-media-query, "#{$breakpoint-key}\\:", null);

  @include breakpoint($breakpoint-key) {
    @each $name, $displayer in $-displayers {
      .#{$breakpoint-prefix}#{$name} {
        display: $displayer !important;
      }
    }
  }
}
<div class="md:hidden">
  <div class="CardReel">
    <div class="Card">...</div>
    <div class="Card">...</div>
    <div class="Card">...</div>
  </div>
</div>

<div class="hidden md:block">
  <div class="Grid -sm:col-2 -lg:col-3 -s3">
    <div>
      <div>
        <div class="Card">...</div>
      </div>
      <div>
        <div class="Card">...</div>
      </div>
      <div>
        <div class="Card">...</div>
      </div>
    </div>
  </div>
</div>

この場合カードの内容をテンプレートの2箇所に記述しなければならないが、テンプレートエンジンを使っていれば問題にならないだろう。

ちなみにReelはカスタムプロパティの表現を代替するのが難しく、また利用頻度も少ないため個別のコンポーネントにすることが多い。

そのほかのIEへの対処

Gridについてはdisplay: gridによる実装ではグリッドアイテムの折り返しをIEで実現できないので、フレックスボックスを用いた独自の実装にしている:

.Grid > * {
  display: flex;
  flex-wrap: wrap;
}

.Grid > * > * {
  width: 100%;
}

/**
 * Columuns variant:
 *
 * <div class="Grid -col-{columns}"></div>
 * <div class="Grid -{breakpoint}:col-{columns}"></div>
 */

$Grid-columns-list: (2, 3, 4);

@each $breakpoint-key, $breakpoint in $mq-breakpoints {
  $uses-media-query: $breakpoint != 0;
  $breakpoint-prefix: if($uses-media-query, "#{$breakpoint-key}\\:", null);

  @include breakpoint($breakpoint-key) {
    @each $columns in $Grid-columns-list {
      $name: col-#{$columns};

      .Grid.-#{$breakpoint-prefix}#{$name} > * > * {
        width: percentage(1 / $columns);
      }
    }
  }
}

/**
 * Spacing variant:
 *
 * <div class="Grid -s{spacing}"></div>
 * <div class="Grid -{breakpoint}:s{spacing}"></div>
 */

@each $breakpoint-key, $breakpoint in $mq-breakpoints {
  $uses-media-query: $breakpoint != 0;
  $breakpoint-prefix: if($uses-media-query, "#{$breakpoint-key}\\:", null);

  @include breakpoint($breakpoint-key) {
    @each $spacing-key, $spacing in $spacings {
      $name: s#{$spacing-key};

      .Grid.-#{$breakpoint-prefix}#{$name} > * {
        margin: ($spacing / 2 * -1);
      }

      .Grid.-#{$breakpoint-prefix}#{$name} > * > * {
        padding: ($spacing / 2);
      }
    }
  }
}

またIEにはflex-direction: columnを利用するとそのフレックスアイテムや子孫の固有のアスペクト比(intrinsic aspect ratios)が維持されないバグがある。StackとSwitcherではflex-direction: columnを利用しているので、フレックスアイテムに対してflex-shrink: 0を明示的に指定することでバグを回避している:

_Stack.scss:

.Stack {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

.Stack > * {
  flex-shrink: 0;
}

_Switcher.scss:

.Switcher > * {
  display: flex;
  flex-direction: column;
}

.Switcher > * > * {
  flex-shrink: 0;
  width: 100%;
}

しかしそれでも予期しないバグはまれに発生するため、そういったときには別コンポーネントを作るなどしてバグを回避するなにかしらの対応をしている。

外部からのスタイル宣言の上書き

実装の汎用化を図ろうといろいろ工夫しても例外的な対応が必要になってしまうのは珍しくない。もっともレイアウトプリミティブによる汎用化は全体の個別性の程度を軽減させるためのメソッドであって、これだけですべてを完全に表現し切るのが目的ではない。Every Layoutはレイアウトプリミティブをプログラミング言語におけるプリミティブなデータ型に例えている。標準的なレイアウトの型を活用することで、すべてのレイアウトは実現できないにしても、無駄なルーティンワークはかなり削減できるだろうという話だ。

モディファイアによってバリエーションを表現するこのアプローチの難点は利用するあらゆる値を中央集権的に管理しなければならない点だ。末端のページでのちょっとしたアドリブのためにコアに手を入れなければならないというような。余白のバリエーションなら比較的規則化しやすいが、Sidebarの幅やSwitchのアイテムの比率などどうしても必要なときになってみるまでわからないものもある。こうした場合には汎用的な解決を考えるのは諦めて、レイアウトプリミティブをラップする個別のコンポーネントなどを作って上書きするようにする:

<div class="MyComponent">
  <div class="Switch">
    <div>
      <div>default</div>
      <div class="MyComponent__featuredItem">featured</div>
      <div>default</div>
    </div>
  </div>
</div>
<div class="MyComponent">
  <div class="Stack">
    <p>Lorem ipsum dolor sit amet.</p>
    <p>Lorem ipsum dolor sit amet.</p>

    <div class="MyComponent__specialItem">
      <div class="Card">...</div>
    </div>

    <p>Lorem ipsum dolor sit amet.</p>
  </div>
</div>

こうすればエッジケースへの対応の影響を局所的にできる。

またレイアウトプリミティブとそれ以外のコンポーネントの境界を明確にするためにレイヤリングを行うこともできる。既存のCSSアーキテクチャでいうとITCSSのObjectsとComponentsのモデルが適しているように思える。ObjectsはOOCSSと同様の装飾がなく汎用的なパターンだ。装飾はComponentsのレイヤーで施され、また汎用性のないスタイルもここに属する。レイアウトプリミティブのみがあらかじめObjectsとして位置していて、プロジェクトの開始後に追加された実装は基本的にはComponentsとして扱うのが良いだろう。

// Center, Cluster, Grid, Stack, Switcher...
@import "objects/*";

// ArticleBody, Card, CardReel, MyComponent...
@import "components/*";

ITCSSの目的はスタイル記述順の制御であり、同じ詳細度の宣言はより後のレイヤーによって上書きされる仕組み。もっともレイアウトプリミティブでは全称セレクタを多用するので、詳細度が高まっていてあまりうまくは機能させられないが……。はっきり区別させる意味では役に立つ。

各レイアウトプリミティブのユースケースの例示

よくある「目に見える」コンポーネントカタログと違って、レイアウトプリミティブのユースケースは最初は少し想像しづらい。その汎用性ゆえに抽象的で、具体例が欲しくなる。その解決のためにパターンごとのユースケースを掲載したスタイルガイドを作成した。

f:id:yuheiy:20200517215124j:plain

_shifted/1-objects.pug at master · yuheiy/_shifted

利用方法はこれらの例から学習できるだろう。

プロジェクトのセットアップ

これまで紹介してきたレイアウトプリミティブの実装はひとつのリポジトリに集約させていて、それらがあらかじめ用意された状態で新しいプロジェクトをはじめられるようにしてある。基本的なパターンを繰り返し実装し直す手間を省いて、個別の問題により集中できるようにする狙いがある。

_shifted/boilerplate-static/app/assets/objects at master · yuheiy/_shifted

カスタムプロパティを用いずに汎用化するのが困難なパターンや、利用頻度が少ないパターンは含んでいない。

ちなみにCSS以外の開発環境構築についても汎用化できないか長らく考えていて、これについてもまたいつか書きたい。

宣伝

この記事では自分なりのEvery Layoutの応用について書いたが、その根底には原著が伝えるもとの意図がなければ成立しない。しかしながらEvery Layoutは英語であり有料コンテンツであるためになかなか紹介しづらく、また読んでもらうハードルも高く、非常にやりきれない思いになっていた。

そうしたところで偶然、編集者の岡本さんにお声がけいただき、Every Layoutを日本語訳して出版する事の運びとなった。友人の横内さんとも一緒に。

原著の内容をなんとかうまく伝えられるよう精一杯やりますので、みなさん何卒よろしくお願いします。

CSSにおける汎用化の先送り、ユーティリティファーストCSS、レイアウトプリミティブ

CSSは普通、セレクタの記述から始まる。目の前にあるHTML片に対してどのようなスタイリングを施すかという前に、いかにしてそのHTML片を選択するかという意識が先に来る。あらかじめ完成したHTML文書へ向けてスタイルを適用していくのであればそれでうまくやれるのかもしれない。だが広く行われているウェブデザインの制作では、まずゴールとして定められた描画結果だけがあり、そこから逆算してHTMLとCSSを書き進めていく。つまり個別の結果だけがある状態で実装に取り掛かることになる。実装のために必要な構造化はたいてい後手に回る。

それでもCSSセレクタから始まることは変わらない。実装を進めるためにはまずセレクタを書かなければならない。セレクタは規則の根幹である。バグを減らし、開発を効率的にするためには、あらゆるスタイリングの意図をセレクタに反映させるのが基本だ。しかし最初から正確にその意図を把握できる機会はまれであり、現実には、無理矢理こじつけた妄想のような規則性を実装してしまう場合も少なくない。それが瞬く間にサイト全体に広まって取り返しがつかなくなることも。

これを回避するためには、コンポーネントなどに局所的な利用を明示するような名前最初はつけておくことだ。再利用性を念頭においたCSS設計では一般に、やや曖昧なコンテンツに依存しない命名が奨められるが、最初からそうするのは時期尚早だと経験上感じられる。まずは再利用性を制限するために、そのコンポーネントが利用される箇所やコンテンツの性質を積極的に反映させる。サイトのホームでニュース記事を表示させているカルーセルならば、「HomeNewsCarousel」のような冗長すぎる名前を選ぶのがむしろ良い。そして同じコンポーネントを別のページやコンテンツについても利用するのであれば、それがはっきりとわかってから、ふたたびその段階で判明しているコンテキストに応じてコンポーネントの名前をつけ直す。たとえばフィーチャーしたい記事をコンテンツ種別ごとの複数のカルーセルの繰り返しによってホームに表示するのであれば「HomeFeaturedCarousel」とか、別ページでもニュース記事を表示させるために使うのであれば「NewsCarousel」とか。

このように利用箇所に応じたコンテキストを明示し、変化があれば見直しの上で追従していく作業を、プロジェクトの生存期間中は半永久的に行う。利用するコードの意図がつねに明快になっている意味では健全だが、難点としてはただ、めんどくさい。特に開発の初期段階ではところ構わず再利用できた方が手数が少なくて楽な場合もある。しかし時に想定よりもはるかに長い期間メンテナンスされ続けるCSSにおいて、いかにすればこのめんどくささを軽減させて継続的な意図の反映を行なっていけるのか。

ユーティリティファーストCSS

ユーティリティーファースト(またはAtomic)CSSと呼ばれるアプローチがある。スタイル宣言と対応する細かなクラスがフレームワークとしてあらかじめひと通り用意されており、ユーザーは基本的には新たにCSSを書かずともHTML上でクラスを組み合わせていくだけでスタイリングが行えるというもの。

もっとも人気の実装であるTailwind CSSでは、たとえばチャットの通知アラートは、ユーザーが新しくCSSを記述しなくても次のHTMLだけで作ることができる。

<div class="max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl">
  <div class="flex-shrink-0">
    <img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div class="ml-6 pt-1">
    <h4 class="text-xl text-gray-900 leading-tight">ChitChat</h4>
    <p class="text-base text-gray-600 leading-normal">You have a new message!</p>
  </div>
</div>

出典:Utility-First - Tailwind CSS

これを利用すれば初手にまず名前を考えるという作業はスキップできる。一見スタイルの再利用性の問題がありそうに思えるが、昨今のプロジェクトではなにかしらのテンプレートエンジンを採用するはずなので、テンプレート機能を用いてマークアップコンポーネント的に管理すれば解決できる。要点は後から共通化できることである。

インラインstyle属性との違いとしては、まずインラインstyle属性では利用できないメディアクエリや擬似クラスがユーティリティークラスとして用意されている点。そして次に各スタイル宣言の値が特定のバリエーションによって意図的に制約されるという点。単にインラインstyle属性を使うのでは、宣言の値は場当たり次第でユニークになってしまうことがある。余白やフォントサイズ、テキストの色など、これらの判断が宣言ごとにバラバラになっているとシステムとしての一貫性がなくなってしまう。

Tailwind CSSはあらかじめ決められた値のバリエーションと対応するユーティリティクラスだけを提供している。たとえば色についてはデフォルトのカラーパレットが設定されていて、カラーパレットにある値だけが色に関するプロパティと対応するユーティリティクラスになっている。提供されるクラスを使う限りはカラーパレットのルールを外れないというわけだ。

最初からこのフレームワークを利用してページをデザインするなら制約として機能するだろう。しかしそれを意識せずにすでにSketchなどのデザインツールでデザインされたページがあったとすれば、当然フレームワークの設定値(デザインシステムの文脈ではデザイントークンと呼ばれる)は意図に沿わない間違った制約になってしまう。ユーザーが任意の値によってデザイントークンを設定できるようになっていたとしても、やはり汎用化の話と同じく、最初から正しいデザイントークンを見つけ出すこと自体が困難だ。仕組み上、ほとんどのユーティリティクラスはデザイントークンとセットになっていないと存在できないので、ユーティリティファーストのアプローチは結果的に成り立たなくなってしまう。

さらにかなりの数が存在するユーティリティクラスの命名規則を覚える必要もある。長期的に付き合っていくプロジェクトではまだしも、そうでない場合に少し関わる程度のメンバーが毎度これに慣れるというのはそれなりの負担になる。

そしてこれはいわば低レイヤーのフレームワークであり、CSSフレームワークというよりはCSSを組み立てるためのフレームワークと表現した方が近い。既存のコンポーネントのようなものは用意されていないので、最初はすべてのものをユーザーが組み上げなければならない。

ユーティリティファーストCSSについてここまでで浮上した問題をまとめると、正しいデザイントークンの発見を前提にしないとアプローチが成り立たないことと、CSSの記述方法を代替する以上のものではないということだ。ではどうすればいいのか?

レイアウトプリミティブ

Every Layoutが提唱するレイアウトプリミティブは、頻繁に出現するレイアウトの最小要素を、レスポンシブデザインを前提としたCSSにおいても再利用可能にしたパターンのこと。

たとえば「The Stack」は、縦方向に繰り返す要素間に共通の余白を挿入するためのパターン。

<div class="Stack">
  <p>Lorem ipsum dolor sit amet consectetur.</p>
  <p>Lorem ipsum dolor sit amet consectetur.</p>
  <p>Lorem ipsum dolor sit amet consectetur.</p>
</div>
.Stack > * + * {
  margin-top: 1.5rem;
}

「The Center」は、要素の幅を特定のサイズを超えないように制限した上で中央に寄せるパターン。

<div class="Center">
  <p>Lorem ipsum dolor sit amet consectetur.</p>
  <p>Lorem ipsum dolor sit amet consectetur.</p>
  <p>Lorem ipsum dolor sit amet consectetur.</p>
</div>
.Center {
  max-width: 40rem;
  margin-right: auto;
  margin-left: auto;
  padding-right: 1rem;
  padding-left: 1rem;
}

このようなパターンが今のところ合計で12個紹介されている

ページには12のリンクがパターンを簡略化したアイコンとともに掲載されている

レイアウトプリミティブの特徴は、パターンの役割がとにかく純粋であること。責務を混合させずに独立させることによって、かなり広範囲の問題に対してパターンが適用できるようになっている。

それぞれのパターンは相互に組み合わせて利用する前提で設計されている。たとえばダイアログは次のような構成で実装できる。

ダイアログはCluster、Stack、Box、Centerの各レイアウトプリミティブで構成されている

登録フォームならこんな感じに。

3つのフィールドと送信ボタンを持つフォームはCenter、Stack、Box、Centerのレイアウトプリミティブで構成されている

あるいは講演でのスライド。

テキストが中央に配置され、前と次のボタンが下部に配置されたスライド。Cover、Box、Stack、Sidebarのプリミティブで構成されている。

いずれも出典は「Composition: Every Layout」より。

個人的な経験則として、レイアウトプリミティブのパターンは実際にかなり多くのレイアウトの実装に適用できる。それぞれのパターンをクラスとして再利用できるようにしておくと、結果的にCSSの総量をかなり削減できる。つまりはCSSを書く場面が減り、新しく作らなければならないコンポーネントや要素の数が減り、命名の機会が減る。もちろん共通化はテンプレートエンジンで行える。

パターンの収集という意味でレイアウトプリミティブは絶妙である。ウェブデザインの中で無意識的に繰り返されていたようなレイアウトの手法を拾い上げ、極めて汎用的な形式知に変換することによって、思いもしない抽象化の可能性が提示されたように感じた。OOCSSの原則であった「構造とスキンの分離」は、ページからそのパターンを発見する困難さゆえに機能しなかった。大袈裟かもしれないが、レイアウトプリミティブはウェブデザインの普遍的なパターンに思える。設計を進めていく最中でパターンを発見していくのには無理があり、あらかじめわかっているパターンを拠り所にできる方が間違いがないだろう。(レスポンシブデザインという制約が昨今のレイアウト規則を画一化した結果とも言えるかもしれない。)

しかし残念ながらEvery Layoutで紹介されている実装はそのままでは現実のプロジェクトには適用しづらい。特定の画面幅への最適化を避けて意図的にメディアクエリによるブレイクポイントに依存しない仕組みになっていたり、IE11で利用できない機能にしっかり依存していたり……。これらにはある程度納得できる理屈がありつつも、業務においても「そういうことで」とするにはかなり無理がある。ただそれでもこのアイデアはなんとか活用してみたかったので、1年近く苦心して、ある程度安定したプラクティスを見つけ出すことができた。それについては次の記事「実践的レイアウトプリミティブ」で紹介する。

参考文献

React Hooksで保持する参照を毎回初期化しないようにする

React Hooksを使ってオブジェクトへの参照を保持する場合、初期値を伴うuseRef()を使うと次のようになる:

const MyComponent: React.FC<Props> = () => {
  const instanceRef = React.useRef(createInstance())
  return ...
}

こうするとコンポーネントが再レンダーされるたびに毎回初期化を行ってしまって無駄な処理になるほか、初期化に副作用があったりすると厄介になる。

useState()によって初期化するとこの問題が回避できる:

// useRef () will initialize a reference on every render.
// useState () allows initialization only on first render.
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
function useLazyInitializableRef<T>(create: () => T): T {
  const [value] = React.useState(create)
  return value
}

const MyComponent: React.FC<Props> = () => {
  const instance = useLazyInitializableRef(createInstance)
  return ...
}

(ちなみにtsx内ではアロー関数にジェネリクスが使えない)

useState()の引数に渡した関数は初回レンダー時のみ実行され、後続のレンダーでは無視される

react-useにあるuseMeasure()の実装を見ていて気づいた。