CSSモジュール ― 明るい未来へようこそ

ここ最近、CSSに対する考え方が広がりを見せています。皆さんの中には、その転換点を見つけようと、Christopher Chedeauの”CSS in JS”という講演を聞いた方もいるでしょう。2014年11月にNationJSで行われたこの講演は、CSSにおける重大な分岐点となりました。まるで高エネルギー粒子が衝突した後のように、それを機に、数ある多様な考え方が、各々の方向へ渦を描くように広がったのです。その例として、React StylejsxstyleRadiumを挙げましょう。これら3つは、Reactのスタイリングにおける最新かつ最良、そして最も実行しやすいアプローチに含まれており、各々のプロジェクトのReadmeファイルでも、そのように言及しています。もし”発明”が、adjacent possible(一歩先にある可能性)を探ることの一例であるのなら、Christopherのおかげで数ある実現可能な事柄が、より身近なものになったと言えます。

Christopher Chedeau's 7 problems with CSS at scale
注釈:
大規模化するCSSの問題点
1. グローバルネームスペース
2. 依存性
3. デッドコードの削除
4. 縮小
5. 定数の共有
6. 非決定論的な解決
7. 分離

このスライドの内容は、多くの人にとって耳が痛いものでしょう。

どの問題も、もっともな内容です。更に多くの場合、大規模なCSSのコードベースに何かしらの形で影響を与える問題です。Christopherは、スタイリングをJavaScriptに移行すれば、これらの問題を全てうまく解決できると指摘しています。彼の意見は正しいのですが、そうするとスタイリング自体の複雑性や特異性が増してしまうことになります。長年にわたりCSSで解決されてきた:hover状態の扱いについて、先ほど私が挙げたプロジェクトで、どのようなアプローチが取られているのか見てみてください。

CSSの良い点を全て残した上で、そこにstyles-in-JSコミュニティが開発した素晴らしい機能を追加する――私たちCSSモジュールチームは、この課題に真っ向から取り組めると信じてきました。私たちは自らのアプローチを固持し、断固としてCSSの美点を主張してきましたが、私たちがそうしている間、CSSの限界を別の角度から押し広げてくれた人々には、深く感謝しています。本当にありがとう!👬👫👭

ではCSSモジュールとは何か、なぜそこに未来があるのかを説明していきましょう。

Jony Ive contemplates CSS Modules
こんな風に私たちはCSSについて真剣に考えてきました。

ステップ1. デフォルトで、すでにローカルである

CSSモジュールでは、各ファイルが別々にコンパイルされるため、一般的な名称のシンプルなクラスセレクタを使うことができます。よって、グローバルスコープを汚染する心配がありません。ここで、以下の4つの状態をもつ簡単な送信ボタンを作成するケースを考えてみましょう。

buttons

CSSモジュールを使う前

Suit/BEMスタイルのクラス名と、ごく普通のCSSとHTMLを使って以下のようなコードを書いたとしましょう。

/* components/submit-button.css */
.Button { /* all styles for Normal */ }
.Button--disabled { /* overrides for Disabled */ }
.Button--error { /* overrides for Error */ }
.Button--in-progress { /* overrides for In Progress */
<button class="Button Button--in-progress">Processing...</button>

これは実に良くできています。4つのバリアントを含んでおり、BEMスタイルの命名規則通り、ネストされたセレクタは使用していません。Buttonの表記を大文字で始めているのは、(願わくは)読み込んでいる以前のスタイルや依存関係で競合が発生しないようにするためです。また--modifierシンタックスを用いることで、バリアントが適合する基底クラスを必要としているということを明示しています。

全体としては、非常に明確で保守性の高いコードです。命名規則関連では、相当の注意を払う必要がありますが、これは標準のCSSにおいて最良の方法です。

CSSモジュールを使った場合

CSSモジュールを使う場合、より一般的な名前を付ける必要はなく、最も意味が分かりやすい名前を使えば大丈夫です。

/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */

ここでは、“button”という単語をどこにも使用していないことに注目してください。一体なぜでしょうか? それは、このファイル自体がすでに“submit-button.css”という名称だからです。どの言語においても、全てのローカル変数の前に、ファイル名を付ける必要などありません。それはCSSでも同じです。

CSSモジュールのコンパイル方法、つまりJavaScriptからファイルを読み込むために、requireimportを使うことによって、これを可能にしています。

* components/submit-button.js */
import styles from './submit-button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

実際のクラス名は自動生成され、一意的であることが保証されます。CSSモジュール側でその点を全て考慮し、ICSS(詳細は私のブログを参照してください)というフォーマットにファイルをコンパイルしてくれます。これにより、CSSとJSが連携できるようになり、アプリを起動すると、以下のように表示されるはずです。

<button class="components_submit_button__normal__abc5436">
  Processing...
</button>

DOMでこの内容が確認できれば成功です! 

A gorilla high-fives a shark in front of an explosion
あなたがゴリラで、CSSモジュールがサメですよ。
(著作権者:Christopher Hastings

命名規則

では、ボタンの例を再考してみましょう。

/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */

ここでは、全てのクラスがスタンドアローンである点に注目してください。1つのクラスが“基底”となり、それを他のクラスが”オーバーライド”しているのではありません。CSSモジュールでは、バリアントに必要な全てのスタイルが、各クラスに含まれている必要があります(この仕組みについては、後ほど説明します)。これにより、JavaScript上で、このようなスタイルを扱う方法が大きく変わってきます。

/* Don't do this */
`class=${[styles.normal, styles['in-progress']].join(" ")}`

/* Using a single name makes a big difference */
`class=${styles['in-progress']}`

/* camelCase makes it even better */
`class=${styles.inProgress}`

もちろん、あなたがキーストローク単位で給料を貰っているのであれば、好きなようにやっていただいていいですよ。

Reactの例

これはCSSモジュールというより、Reactの仕様の話になりますが、Reactを使うことで、CSSの使用感が飛躍的に向上するので、少し複雑な例を出す価値もあるでしょう。

/* components/submit-button.jsx */
import { Component } from 'react';
import styles from './submit-button.css';

export default class SubmitButton extends Component {
  render() {
    let className, text = "Submit"
    if (this.props.store.submissionInProgress) {
      className = styles.inProgress
      text = "Processing..."
    } else if (this.props.store.errorOccurred) {
      className = styles.error
    } else if (!this.props.form.valid) {
      className = styles.disabled
    } else {
      className = styles.normal
    }
    return <button className={className}>{text}</button>
  }
}

スタイルを使う際に、グローバルで使えるCSSのクラス名がどんな名前で生成されるかを気にする必要はありません。つまり、スタイリングではなく、コンポーネントに集中できるのです。このように、常にあったコンテキストスイッチが取り除かれると、それまで耐え忍んできた状況に唖然とすると思います。

しかし、これはまだ始まりに過ぎません。スタイルがどう統合されるかを考える段階では、CSSモジュールが役立ちます。

ステップ2. 何と言ってもコンポジション

先ほど、各クラスには全ての状態に対応したボタンのスタイルが含まれるべきだと述べました。これは複数のスタイルを持たないと見なすBEMとは正反対です。

/* BEM Style */
innerHTML = `<button class="Button Button--in-progress">`

/* CSS Modules */
innerHTML = `<button class="${styles.inProgress}">`

ところで、全ての状態間で共有されたスタイルをどのように表現するのでしょうか。その答えは、おそらくCSSモジュール最大の武器であるコンポジションです。

.common {
  /* all the common styles you want */
}
.normal {
  composes: common;
  /* anything that only applies to Normal */
}
.disabled {
  composes: common;
  /* anything that only applies to Disabled */
}
.error {
  composes: common;
  /* anything that only applies to Error */
}
.inProgress {
  composes: common;
  /* anything that only applies to In Progress */
}

このcomposesキーワードは、「.normal.commonの全てのスタイルを含む」という意味で、Sassでいう所の@extendsキーワードにあたります。SassはCSSセレクタを上書きすることによってこれを行うのに対し、CSSモジュールはどのクラスをJavaScriptにエクスポートすべきかを変更しています。

Sassを使う

上記のBEMの例に、Sassの@extendsを適用してみましょう。

.Button--common { /* font-sizes, padding, border-radius */ }
.Button--normal {
  @extends .Button--common;
  /* blue color, light blue background */
}
.Button--error {
  @extends .Button--common;
  /* red color, light red background */
}

これが以下のCSSにコンパイルされます。

.Button--common, .Button--normal, .Button--error {
  /* font-sizes, padding, border-radius */
}
.Button--normal {
  /* blue color, light blue background */
}
.Button--error {
  /* red color, light red background */
}

これで、マークアップ内で1つのクラス<button class="Button--error">だけを使い、共通するスタイルと固有のスタイルの両方を思い通りに付与することができます。実に有力な概念ですが、この実装にはエッジケースや落とし穴もあるので、注意が必要です。具体的な問題の概要と参考文献へのリンクは、Hugo Giraudelによるこちらで確認することができます

CSSモジュールを使った場合

キーワードcomposesは、概念的には@extendsと同じですが、異なった働きをします。実際にやってみますので、以下の例を見てください。

.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }

コンパイルされたコードは、ブラウザに到達するころには、以下のようになります。

.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 { /* blue color, light blue background */ }
.components_submit_button__error__1638bcd { /* red color, light red background */ }

JavaScriptのコードでは、import styles from "./submit-button.css"が以下を返します。

styles: {
  common: "components_submit_button__common__abc5436",
  normal: "components_submit_button__common__abc5436 components_submit_button__normal__def6547",
  error: "components_submit_button__common__abc5436 components_submit_button__error__1638bcd"
}

 styles.normalstyles.errorのコードも変わらず使うことはできますが、複数のクラスがDOMにレンダリングされます。

<button class="components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

これはcomposesがもたらす効果で、マークアップを変更したり、CSSセレクタを書き換えたりすることなく、複数の独立したスタイルのグループを統合することができます👌

ステップ3. ファイル間の共有

SassまたはLESSでは、@importする各ファイルは、同じグローバルワークスペースで処理されます。これによって、1つのファイル内で変数またはミックスインを定義することができ、全てのコンポーネントファイルで使えます。便利ではありますが、変数名が互いに競合の危機にさらされると(他のグローバルネームスペースであるため)、必ずvariables.scssまたはsettings.scssのリファクタリングを余儀なくされます。すると、どのコンポーネントが、どの変数に依存しているかという可視性を失うことになり、設定ファイルは扱いにくくなってしまいます

より最適な方法もありますが(実際、Ben Smithettが投稿したSass & Webpackを一緒に使うことについての記事は、CSSモジュールプロジェクトに直接的な影響を与えていますので、是非読んでみてください)、依然として、Sassのグローバルな性質によって制約されます。

CSSモジュールが一度に起動できるのは、1つのファイルに対してです。ですから、グローバルコンテキストが汚染されることはありません。それに、依存をimportしたりrequireしたりすることができるJavaScriptのように、CSSモジュールは他のファイルからcomposeしてくれます。

/* colors.css */
.primary {
  color: #720;
}
.secondary {
  color: #777;
}
/* other helper classes... */
/* submit-button.css */
.common { /* font-sizes, padding, border-radius */ }
.normal {
  composes: common;
  composes: primary from "../shared/colors.css";
}

コンポジションを使うと、colors.cssといったような、一般的なファイルにたどり着くことができ、ローカル名として使いたいクラスを1つ参照してくれます。コンポジションは、CSSそのものではなく、どのクラスがエクスポートされるのかを変更するので、ブラウザに到達する前にcomposesステートメントそのものは、CSSから消去されます。

/* colors.css */
.shared_colors__primary__fca929 {
  color: #720;
}
.shared_colors__secondary__acf292 {
  color: #777;
}
/* submit-button.css */
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 {}
<button class="shared_colors__primary__fca929
               components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

実際、ブラウザに到着する頃には、ローカル名である”normal”には、独自のスタイルがなくなっています。これは良いことです! つまり、新たにCSSの行を1つも追加することなく、ローカルで認識することのできるオブジェクト(”normal”と呼ばれるエンティティ)を追加することができるのです。この作業を多く行うことができれば、サイトに紛れ込む視覚的な矛盾が発生する頻度が軽減され、お客様のブラウザを肥大化させなくて済みます。

余談ですが、これらの空のクラスは、簡単に検知することができ、cssoなどで消去することができます。

ステップ4. モジュールの単一責任

コンポジションは、構成されているスタイルではなく、要素が何であるかを説明してくれるので、効果的です。概念エンティティ(要素)をスタイルエンティティ(ルール)にマップイングするには異なる方法を取ります。では、簡単な例をごく普通のCSSで見ていきましょう。

.some_element {
  font-size: 1.5rem;
  color: rgba(0,0,0,0);
  padding: 0.5rem;
  box-shadow: 0 0 4px -2px;
}

要素、スタイル共にシンプルです。しかし、問題があります。他でもこのスタイルを使うことがあるかもしれないのに、色やフォントの大きさ、ボックスの影、パディングなど、全てが詳細に指定してあります。ではSassを使ってリファクタリングしてみましょう。

$large-font-size: 1.5rem;
$dark-text: rgba(0,0,0,0);
$padding-normal: 0.5rem;
@mixin subtle-shadow {
  box-shadow: 0 0 4px -2px;
}

.some_element {
  @include subtle-shadow;
  font-size: $large-font-size;
  color: $dark-text;
  padding: $padding-normal;
}

改善されました。しかし、行の半分を抽出しただけです。$large-font-sizeがタイポグラフィ、$padding-normalがレイアウトに対する指定であるという事実は、名前によって表現されただけで、どこにも強制力がありません。box-shadowのように、宣言の値が変数にならない場合、@mixinまたは@extendsを使う必要があります。

CSSモジュールを使った場合

コンポジションを使って、再利用という観点から、コンポーネントを宣言します。

.element {
  composes: large from "./typography.css";
  composes: dark-text from "./colors.css";
  composes: padding-all-medium from "./layout.css";
  composes: subtle-shadow from "./effect.css";
}

フォーマットは、単一目的ファイルがたくさんあり、異なる目的別にスタイルを記述するために、名前空間ではなくファイルシステムを使うことに適しています。複数のクラスを単独のファイルから作成したい場合は、以下のように簡素化することができます。

/* this short hand: */
.element {
  composes: padding-large margin-small from "./layout.css";
}

/* is equivalent to: */
.element {
  composes: padding-large from "./layout.css";
  composes: margin-small from "./layout.css";
}

これは、サイトが使用する全ての可視性のあるトレイトに対して別名を与えるために、極度に粒度の細かいクラスを使うという、一つの可能性を広げることになります。

.article {
  composes: flex vertical centered from"./layout.css";
}

.masthead {
  composes: serif bold 48pt centered from "./typography.css";
  composes: paragraph-margin-below from "./layout.css";
}

.body {
  composes: max720 paragraph-margin-below from "layout.css";
  composes: sans light paragraph-line-height from "./typography.css";
}

これは、私が更に掘り下げたいと思っている手法です。頭の中では、Tachyonsといった、アトミックなCSSの手法の最も良いところと、Semantic UIIのように、正確で信頼性のある、読みやすいものを組み合わせた感じと思っています。

ですが、CSSモジュールはまだ始まったばかりです。まずは、あなたの次のプロジェクトで実際に試してみてください。そして今後、内容を具体化していくために、私たちに協力していただけたら嬉しいです。

始めてみよう!

CSSモジュールを使うことによって、あなたが今持ち合わせているCSSの知識やプロダクトの維持の手助けになれば幸いです。より快適で生産性のあるものになるでしょう。ここでは、シンタックスの使用を最低限に抑え、あなたが現在作業しているものに似通った例を使用するように努めました。WebpackJSPMBrowserifyを使っているのであれば、リンクからデモ用のプロジェクトをご覧いただくことができます。私たちは、CSSモジュールが機能する新しい環境を常に探しています。サーバーサイドのNodeJSのサポートについては、現在対応中ですし、Railsについては、近い将来対応する予定です。

皆さんがより理解できるように、実際に試せる用例をPlunkrに投稿しました。インストールなどは不要なので、是非試してみてください。

Try CSS Modules live

自信がついたら、メインのCSSモジュールリポジトリも確認してみてください。質問があれば問題を上げていただき、直接話し合っていきましょう。CSSモジュールチームは小規模なので、全ての使用例を見切れてはいませんが、皆さんからの意見をお待ちしています。

スタイルの楽しさを皆さんにも!