エンジニアをリングする

プログラをミングしたり。

my web site twitter

SPAにおけるCSSについて、ひとつの解(追記あり)

この話を簡単にまとめておこうと思います。
結論を先に書くと「今のところtemplate literal内にCSSを記述する形式のCSS in JSがいい感じ。Reactならstyled-componetnsが良かった」という感じです。

悩んでいたこと

コンポーネント指向でSPAを作っていく上で、CSS(というかスタイリング)はどう書いていったらいいんだろう?ということに結構悩んでいました。
HTMLとJSがコンポーネントとしてまとまっていく中でCSSだけは今まで通り別物として扱い、BEMなどでグローバルスコープと戦うのか?はたまたCSSの枠をはみ出てJSコンポーネントの粒度に合わせたコンポーネント化をするのか?
加えて、見た目も挙動も複雑なアプリケーションを作っていく上では以下の振る舞いが欲しくなってきます。

  • コンポーネントごとにスコープが切られる
  • JSとCSSで値を共有できる
  • そのスタイルがどこで利用されているのか把握できる

こうなるとやはりCSS ModulesやCSS in JS的なアプローチになってくるか…と思ったのですが、ロックイン回避や乗り換えのことを考えると同時に以下も意識したいところです。

  • スタイルのsyntaxが標準のCSSから乖離しない
  • エディタからCSSと同等のサポートを受けられる (syntax highlightや補完など)
  • JSが特定のツールに依存せずvalidである

そして最近「template literal内にCSSを記述する形式のCSS in JS」がこれらの要件を満たしてくれることに気付いたので、記事を書くことにしました。

template literalを使用したCSS in JSってどんなの?

ライブラリによってそれぞれ色があります。
CSS in JSの比較表である https://github.com/MicheleBertoli/css-in-js の中からstar数の多いライブラリのコード例を3つ載せてみます。

styled-components

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

styled-jsx

export default () => (
  <div>
    <p>only this paragraph will get the style :)</p>
    <style jsx>{`
      p {
        color: red;
      }
    `}</style>
  </div>
)

csjs

const green = '#33aa22';
const styles = csjs`
  .panel {
    border: 1px solid black;
    background-color: ${green};
  }
  .title {
    padding: 4px;
    font-size: 15px;
  }
`;

私はReact案件では最初のstyled-componentsを使っています。
あとAngular2以降もtemplate literalの形式を採用していますね。

template literalを使用したCSS in JSだと何が嬉しいの?

最初に挙げた要件を満たしてくれるのが嬉しいです。
挙げた要件をひとつずつ見ていきたいと思います。
まず最初の3つはtemplate literal関係なくCSS in JSのメリットです。

コンポーネントごとにスコープが切られる

CSS in JSは大抵グローバルスコープではなくひとつのスタイルのまとまり単位でスコープを切ってくれます。 それにより「この名前どこかで使ってるかも…」という心配が不要になり、ネーミングコストやgrepコストが激減します。
クラス名の衝突については「適切なCSS設計をしていれば大丈夫」というのもあるのですが、むしろ今までグローバルスコープと戦う役割も担っていたCSSでの設計がそこから開放されることで、もっと違う課題にフォーカスして形を変えていくのではないかと思っていて、少し楽しみだったりします。

JSとCSSで値を共有できる

複雑なアニメーションやドラッグでの要素の移動がたくさん出てくるようなリッチめなアプリケーションだと、どうしてもこれが欲しくなってきます。
// style.cssの.hoge-classのxxxの値と合わせるみたいなコメントはもう見たくない…!!

styled-componentsではこんな感じで共有することができます。

// style定義側
const Box = styled.div`
  width: ${({ width }) => width}px;
  height: ${({ width }) => width * 2}px;
`;

// 使用する側
const width = 300;
<Box width={width}/> // 300px×600pxのdivになる

template literalの中に ${() => } のように無名関数を埋め込むことで、引数にpropsが渡ってきます。無名関数の返り値がその部分の値として利用されることになります。 ちょっと見た感じがごちゃつきますが、valid JSな中で最小限にスマートに実現できているのがいいなと思います。
CSSの標準Syntaxからは離れてしまいますが、どちらをとるかというところですね…

そのスタイルがどこで利用されているのか把握できる

「一度書いたCSSは消せない」の理由がこれですね。。
通常のCSSだとそのスタイルがどこでどう使われているのかぱっとわからないので、どこに影響が出るかわからず消すに消せない、という問題があります。 CSS in JSならスタイル定義はJSの形式でimportしてくるので、参照先のスタイル定義が消されてしまったら静的にエラーになります。
逆にこのスタイル定義がどこで使われているかというのもexportしたオブジェクトがどこからimportされているかがわかればいいので、JS向けのツールやIDEの機能を活用できます。 ある定義がいつの間にかどこからも参照されなくなっている、というケースもIDEのハイライトやlinterでかなり見つけやすくなるでしょう。

そしてここからがtemplate literalの強みです。

スタイルのsyntaxが標準のCSSから乖離しない

ここまでで挙げた3つの要件はCSS in JSなら解決できるのですが、その採用にあたってはこれが一番の障壁でした。
CSS in JSによく見られるアプローチに、スタイルをJSのオブジェクトリテラルで書いていくというものがあります。

const styles = StyleSheet.create({
    blue: {
        backgroundColor: 'blue'
    },
    hover: {
        ':hover': {
            backgroundColor: 'red'
        }
    },
    small: {
        '@media (max-width: 600px)': {
            backgroundColor: 'red',
        }
    }
});

(star数が一番多そうな https://github.com/Khan/aphrodite より)

私がよく見かけていたCSS in JSライブラリの形式はほとんどこれだったので、最初は「CSS in JSはCSSをJSオブジェクトとして記述する」と誤解していたほどです。
JSオブジェクトの形式だと

  • syntaxがCSSの標準からだいぶ外れる
    • 通常のCSSのsyntaxに移行したくなったとき変換したりが面倒そう
    • sketchやzeplinからbox-shadowの指定をコピペとかできない
  • JSオブジェクトなのでCSSではクォーテーションが不要なはずの箇所でも必須
    • 書くのが面倒そう

このような理由から個人的にはなかなか乗っかりにくいなと感じていて、グローバルなAltCSSに甘んじていました。でもそれはそれでコンポーネントからCSSだけ置いてけぼり感が強く、つらい…。。

しかしCSS in JSにはオブジェクトリテラルのライブラリだけではなく、styled-componentsのようにES2015のtemplate literalを使用したライブラリもあるのです。 これなら中にはCSSのsyntaxをそのまま記述することができ、なおかつJSとして扱えます。
見た目はなかなか異物感がありますが、なるほどなーと思いました。

エディタからCSSと同等のサポートを受けられる

これはちょっとハック的な感じなので余談なのですが。
syntax highlightや補完がCSS(AltCSS)と同じように効くかというのも開発効率の上で重要なポイントです。
私が使っているWebStormではLanguage injectionという機能を使ってここをカバーすることができています。

// language=SCSS
const Title = styled.h1`
  & {
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
  }
`;

これで中括弧の中ではCSS(SCSS)としてのsyntax highlightや補完やlintが効くようになります。CSSのEmmetも効きます。

こんな感じ

ハックっぽい上にtemplateの中にも外にも不要な記述が必要になってしまいますが、背に腹は代えられない感じですね…できればJSXのように市民権を得てエディタでサポートされるようになると嬉しいなぁって思ってます。

JSが特定のツールに依存せずvalidである

もう一つの選択肢であるCSS Modulesのネックがここでした。
JSに import styles from "./style.css"; とか書きたくなかったんですよね。。。もちろんwebpackに依存しちゃうのも嫌で。。 styled-componentsだとJSが完全にvalidであるところも好きなポイントです。

※ 私は挙げてきたような理由からCSS Modulesやobject literalタイプのCSS in JSを本格的に本番採用したことがないので、もしかしたら挙げたデメリットの中で認識が違ってるところがあるかもしれません。その場合は教えてもらえればと思います。

template literalのライブラリの中でもなんでstyled-componentsがおすすめなの?

styled-componentsを選んだ理由はその形式のライブラリの中で一番star数が多かったことと、styled-componentsの特徴である既存のコンポーネントにスタイルをあてるのではなく独立したスタイル用のコンポーネントを作っていくというアプローチが良いものだと思ったからです。 ただこれはこの記事で紹介した内容とはまた違って、よりCSS設計的な側面の話になるので、今後別途記事を書いて紹介できればなと思っています。

まとめ

こんな感じの考えを経て今styled-componentsを業務で採用し、快適にスタイリングしています。
SPAにおけるCSSはまだデファクトスタンダードになっているものもなく皆それぞれ最適解を探しているような雰囲気があるので、数ある解のうちのひとつとして参考になればと思います。

また、今回はCSSを代替するライブラリ選定の話でしたが、それに加えてJSフレームワークやその他ツールも含めた全体的なSPAにおける技術選定の方針や内容の話をこちらのイベントでする予定ですので、よろしければ。
【React.js】Goodpatch×TeamSpirit Meetup - connpass

追記:

転職によってチーム編成が変わったこともあり、2018年頃からCSS Modulesを選定するようになりました。

得たもの:

  • より標準に沿ったsyntax(validなCSSファイル)
  • 動作時のパフォーマンス

失ったもの:

  • ビルド設定の簡潔さ

評価軸そのものはあまり変わっておらず、上記メリデメのどちらをとるかの価値観が変わった感じです。
こちらのスライドもご参考ください。

SPAにおけるCSSについて、もうひとつの解 - @yoshiko_pg