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

ユーティリティファースト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というアプローチ自体が「決定を遅延させる」考え方だと言える。しかしながら、ユーティリティファーストなアプローチを取るためにはこのデザイントークンだけは先に決まっていなければならないジレンマがある。このあらかじめの設定を意味のある制約だと言う人もいるが、少なくとも「決定を遅延させる」指向とで議論を分けるべきだろう。