CSS Modules 所感

CSS Modulesという、CSSの新しい設計概念・指針のようなものがある。 CSS Modulesチームの1人であるGlen Maddern氏が書いた「CSS Modules - Welcome to the Future」という記事の翻訳がバズっていたので、僕がCSS Modulesについて思ったことをまとてみる。「CSS Modulesとは何か」ということは、上記の記事に書かれているのでここではあまり触れない。

CSS Modulesとコンポーネント設計

CSSのルールセットは全てがグローバル定義であり、CSSCascading Style Sheets)というスタイルシート言語の最大の特徴である"カスケーディング"という機能により、CSSファイルを見ただけでそのスタイルの影響範囲を理解することは難しい。 CSS Modulesは、CSSのルールセットの影響範囲を、Webアプリケーションのいわゆるコンポーネント単位で閉じようという考えで作られた。

ReactやAngular等の主要JSフレームワークは、コンポーネント設計を強要する。 またBEMやSMACSSといったCSSの設計手法も、コンポーネント単位でのスタイリングを意識させる。 このように、Webアプリケーションのコンポーネント設計は、今やかなり当然のことになっているように思う。

CSS Modulesチームの人々はReactユーザーであり、CSS ModulesもReactを使うことを前提に開発されているように思う。 CSS Modulesの簡単な例を示すと、以下のCSSファイルで定義されたルールセットの影響範囲を以下のReact Componentを定義したHTML(JSX)の中で閉じることができる。

/* Logo.css */
.logo {
  background: url('./logo.svg');
  background-size: 200px 200px;
  width: 200px;
  height: 200px;
}
import styles from './Logo.css';

import React, { Component } from 'react';

export default class Logo extends Component {

  render() {
    return <div className={styles.logo} />;
  }

};

グローバルなルールセットの定義の仕方や、コンポーネント間でのスタイルの再利用(ローカルスコープのルールセットの再利用)も可能である。 CSS ModulesのGitHubREADMEや、デモのリポジトリにサンプルコードがあるのでそれらを見てみるといい。

CSS Modulesの仕組み

CSS ModulesがどのようにしてCSSにローカルスコープを与えているのかについて。 CSS Modulesの実装は内部でcss-loaderというwebpack loaderを使っており、言語拡張としていくつかPostCSSのプラグインを作っている。(css-modulesifyというBrowserifyでCSS Modulesを使うためのプラグインも作られてはいるが、現在はhighly experimentalということになっている)

css-loaderは、

:local(.localClass) {
  color: green;
}

:local(.subClass) {
  color: red;
}

:global(.globalClass) {
  color: blue;
}

このように書いたCSS

._23_aKvs-b8bW2Vg3fwHozO {
  color: green;
}

._13LGdX8RMStbBE9w-t0gZ1 {
  color: red;
}

.globalClass {
  color: blue;
}

このように変換する。 セレクタ名をデフォルトではBase64エンコードし、以下のようにexportsすることで、JavaScriptからCSSを参照できる。 webpack.config.jscss-loader?localIdentName=[name]__[local]___[hash:base64:5]のように書くことで、変換後のセレクタ名を指定することもできる。

exports.locals = {
  className: "_23_aKvs-b8bW2Vg3fwHozO",
  subClass: "_13LGdX8RMStbBE9w-t0gZ1"
}

:local()に書かれたセレクタエンコードされることで他のルールセットのセレクタ名と衝突しなくなり、結果的にローカルスコープを得たようになる。 :global()に書かれたセレクタはそのまま展開され、通常のCSSのルールセットと同じ振る舞いをする。

しかし、CSS Modulesでは全てのルールセットはデフォルトでローカル定義となる。 CSS Modulesでは、普通に書かれたセレクタcss-loaderで変換できるように:local()の中に突っ込む処理をしていて、これをpostcss-modules-local-by-defaultというPostCSSのプラグインが担当している。

他にも、CSS Modulesの

element {
  composes: large from "./typography.css";
}

このようなcomposesキーワードによるコンポーネント間でのスタイルの再利用のための構文は、postcss-module-scopepostcss-modules-extract-importsで実現されている。 これら3つのプラグインを使ったCSS ModulesのPostCSSのプラグインパック(css-modules-loader-core)もある。

css-loaderpostcss-modules-local-by-defaultを使うと、上記のLogo.cssを以下のように変換できる。

.Logo__logo___3xrQQ {
  background: url(data:image/svg+xml;base64,...
  background-size: 200px 200px;
  width: 200px;
  height: 200px;
}
module.exports = {
  logo: "Logo__logo___3xrQQ"
}

CSSからJSに互換性を保ったまま情報を渡すことができる上記のフォーマットを、ICSS(Interoperable CSS)と呼んでいる。

CSS in JS系ツール 所感

ここまで長々とCSS Modulesのコンセプトや動作する仕組みについて述べてきたが、本当に伝えたいのはここから。

まず、CSS in JS系のツールについて。 今のCSS in JSの勢いに火がついたのは、Facebook@Vjeux氏の「React: CSS in JS」という発表からだと感じている。

この発表で@Vjeux氏はCSSの問題点をいくつか挙げ、これらをReactのInline Stylesを使って解決できると述べた。 そして、この発表を受けていくつかのCSS in JS系ツールが作られている

インラインに(HTMLのstyle属性に)CSSを展開すると、全てのルールセット(ルールセットではないが)の詳細度を一定に保つことができ、セレクタも存在しないのでカスケーディングの一部を禁止することもできる。 しかし、僕はインラインにCSSを展開する系のツールクソ 微妙だと感じている。

理由はこんなところだ。

  • プロパティー宣言しか書けない(擬似クラスもメディアクエリーも使えない)
  • ネットワークはWebアプリのパフォーマンスのボトルネックとなるので、CSSファイルとしてキャッシュしたい
  • valueが継承されるプロパティーのカスケーディングは結局止められない
  • CSSerとしての本能が受けつけない

あと、JSコード内にJSオブジェクトとしてスタイルを記述するものもあるが、シンタックスハイライトが適切でなかったりするのでそれも嫌だ。

CSS Modules 所感

その点、CSS Modulesは他のCSS in JS系ツールよりも良いものだと思う。(CSS ModulesをCSS in JS系ツールに含めるかは微妙なところだが。) 擬似クラスとメディアクエリーも使えるし、普通にCSSを書いている感覚でコンポーネントごとにローカルスコープを持つことができる。

しかし、CSS Modulesの一般的というか、ユーザーとしての感想を述べると、BEM等のCSSの設計手法になじんでいる人にとっては必要ないと感じた。

CSS ModulesはReact Componentの分割粒度でCSSを書くことを強制する力があって、かつそこにはスコープがあるのでセレクタの命名をそこまで意識しないで適当につけても破綻することがなくなる。

import button from './Button.css';

import React, { Component } from 'react';

export default class Button extends Component {

  render() {
    return <button className={button.is-success} />;
  }

};

CSS Modulesを使うとButton.cssをJSでimport button from './Button.css';のようにimportして、className={button.is-success}のようにその中のスタイルを参照できる。

しかしこれは、プリフィックスがJSコード側に付いただけで、BEMで.Button--successのような名前を付けているのと変わらない。 composesについても、結局はHTMLのclass属性に複数のICSSのセレクタ名が書かれる(マルチクラス)だけだ。 また機能の異なるコンポーネントから、(たまたま表現したい見た目が同じだが)デザインの意図の異なるスタイルをcomposesすると、コンポーネント化が途端に崩れ、雑にCSSを書いているのと同じになる。

CSS Modulesは現状Reactとwebpackの使用が前提にあるし、CSSコンポーネント設計が身についている人(がいるチーム)にとってはわざわざ導入すべきものではないように感じた。 また、CSS Modulesのcomposesを使いこなせる人は、CSSプリプロセッサーのSassで言う@mixin@extendも使いこなせると思う。

CSS Modulesは、「Reactを使って正しくコンポーネントにわけて設計でき、CSSの設計手法やカスケーディングやセレクタ詳細度の仕様をきちんと理解できていないけど、なんとなく破綻しにくいCSSを書きたい」人に向いていると思った。

あわせて読みたい

その他雑感

CSS ModulesチームはCSSの問題点をよく理解していて、本質的な問題解決に挑戦しているように見えた。 僕も「作れるから作ってみた」ではなく、このような考えでプロダクトを作っていきたいと思う。