CSS in JSはCSSの書き方をどのように変えるのか
CSSの難しさの根源はセレクタにある。CSS設計のための方法論ではどのようにしてセレクタと関わるべきかについて語られる。
その関わり方がCSSのみで実現できなければならないという制約を捨てたのがいわゆるCSS in JSの類(定義的に微妙なやつも全部ひっくるめて)だ。可能性は一気に広がり無数のライブラリが生み出された。
ある程度の期間を経ていくつかの着目すべきアプローチが見えてきた。これから僕はどのようにセレクタと関わっていくべきかという視点で記してみたい。
擬似スコープ
通常CSSのセレクタにはスコープはないが、HTMLやCSSにハッシュ値を付与して特定のコンテキストを擬似的に閉じてしまおうというアイデア。実装としては、Vue.jsの単一ファイルコンポーネント、Angularのコンポーネントスタイル、styled-jsxなど。関連するウェブ標準技術としてShadow DOMがある。
例えば、次のように書かれたスタイル宣言は同一コンポーネント内にしか適用されない。
<template> <p>Hello World!</p> </template> <style scoped> p { font-size: 2em; text-align: center; } </style>
このコンポーネントの外にp
要素があったとしても適用範囲外になる。
書き方としては、CSS in JSの類のアプローチの中では最もこれが普通のCSSに近い。スコープがあるということ以外、実質的に変わりがない。セレクタがグローバルであるという難しさを解消するためにスコープを作るということは、発想としてごく普通で受け入れられやすい。
BEMで規約として作っていたブロックというルールを仕組みとして取り入れたとも言える。BEMらしき姿は見えなくなっていても、これはより安全で楽になったBEMだ。
安パイだと思う。
スタイル宣言が付属したHTML要素を作る
styled-componentsを使うと、最初にスタイル宣言が付属したReactコンポーネントを作成し、それを配置していくという形でスタイリングを行うことになる。
import React from 'react' import styled from 'styled-components' const Wrapper = styled.button` background-color: lightgray; border: 1px solid gray; ` const Icon = styled.img` width: 1.25em; height: 1.25em; ` const Text = styled.span` margin-left: 1em; ` const Button = () => { return <Wrapper type="button"> <Icon src="/icon.svg" alt="" /> <Text>click me</Text> </Wrapper> }
従って、個々の宣言ブロックの中身を実装した後に作成した要素を配置して画面を組み上げていくという流れになるのが自然だ。通常、スタイリングはHTMLを書いた後に行う。対して、styled-componentsは宣言の記述のために個々の要素のReactコンポーネント化を要求するため、全体のHTMLが完成する前に個々の宣言ブロックの中身を実装することに意識を向けなければならない。
しかし実際にそのような流れで実装することには無理がある。そのためstyled-componentsを利用したスタイリングには不自然さが伴う。
宣言ブロックの中身は適用される対象の要素だけを見ても完成させられない。継承させるプロパティの兼ね合いや、兄弟要素とのレイアウト上の関係性など、対象となっている要素以外の要素も意識しなければどのような宣言をするかは決められない。
またこうして作成したReactコンポーネントを配置していく際にも難がある。どこにどの要素を配置するかは、それぞれのHTML要素の種類に依存して決まる。にも関わらずこれらを配置していく際には要素の種類が一目ではわからなくなる。さらには単なるスタイルが付属するコンポーネントなのか別の場所から読み込んだ真っ当なコンポーネントなのかも不明瞭になる。
分解したReactコンポーネントを利用するとき、その中の実装が見えないというゆえの扱いにくさがある。先述したようなHTMLやCSSとしての構造の問題がその一部だが、大抵はそれを上回るだけのコンポーネント化による利点がある。しかしstyled-componentsは全てのHTML要素をReactコンポーネント化することを要求してくる。剥き出しになっていて欲しい部分も覆い隠されてしまう。
最初にHTMLだけを組み上げた後に個々の要素をstyled-componentsに置き換えることもできるが、明らかに非効率的だ。結局は読みにくくもなってしまう。
ごく小さなコンポーネントであればともかく、HTMLとしてそれなりの大きさのコンポーネントを実装しているとまるでもう無理になってしまった。
HTML要素にスタイル宣言をリンクする
宣言ブロックを基にしてそれに対応するユニークなクラス名を生成するというアイデアもある。そのクラス名はHTML要素のclass
属性を通してリンクされる。
/* style.css */ .wrapper { background-color: lightgray; border: 1px solid gray; } .icon { width: 1.25em; height: 1.25em; } .text { margin-left: 1em; }
// button.js import styles from "./style.css" const Button = () => { return <button className={styles.wrapper} type="button"> <img className={styles.icon} src="/icon.svg" alt="" /> <span className={styles.text}>click me</span> </button> }
これはCSS Modulesの例。
生成されるクラス名をCSSファイルから読み込み、対象の要素と宣言ブロックを直接繋ぎ合わせる。CSSファイル内のセレクタのようなものはクラス名がマッピングされるオブジェクトのキーに過ぎない。
セレクタをユーザーから隠して宣言ブロックと直接リンクさせるようにしたという意味ではstyled-componentsに近い。このアイデアが優れているのは、単にリンクさせるようにしただけであるというところ。
セレクタというのは書きやすくかつ読みにくいものだ。付属する宣言ブロックがどの要素に適用されるものなのか、実際に実行してみるまで本当に信頼はできない。このアプローチでは、スタイルを適用するためにはクラス名となる文字列を対象の要素に直接繋ぎ合わせるという前提ができることで、コンパイル前の段階でその関係性を確実に保証できる。
ただしCSS Modulesはビルドを複雑にしてしまう。styled-componentsが流行った理由は採用の手軽さにもあるのだろう。
幸い、このようにスタイル宣言をリンクさせるというアプローチが可能なライブラリとしてemotionがある。emotionはstyled-componentsと同等の機能の他に、The css Propという宣言ブロックを基にユニークなクラス名を生成できる機能を備えている。
import React from 'react' import { css } from 'emotion' const wrapperClass = css` background-color: lightgray; border: 1px solid gray; ` const iconClass = css` width: 1.25em; height: 1.25em; ` const textClass = css` margin-left: 1em; ` const Button = () => { return <button className={wrapperClass} type="button"> <img className={iconClass} src="/icon.svg" alt="" /> <span className={textClass}>click me</span> </button> }
さらにこのようにJavaScriptでスタイル宣言を管理することで、未使用の宣言ブロックを検出できるようになるという利点がある。ESLintやTypeScriptを使用すれば、デザインの変更時などに不要になったスタイルを確実に取り除けるようになる。コードを整理するという観点から、これまでのCSSではとても実現できなかったことだ。
感想
CSS in JSとかそんなものやめてしまえけしからんという気持ちもわかる。けど、これまでのCSSで事足りるというのは大抵、道具として完璧で最高にフィットしてるという感じでなくて、まあまあ不満もあるけど妥協してやっていけるよくらいの温度感のはずだ。
僕自身これ系のものを割と食わず嫌いしていたけどそれなりに学ぶこともあった。ので、とりあえずこれでなにか書いてみればよいのではという感想。