次の時代のCSS in JSはWeb Componentsを従える

公開:2020/10/06
更新:2020/10/06
8 min読了の目安(約7600字TECH技術記事
Likes128

CSS in JSはJavaScriptのコード中にCSSを書くプログラミング手法で、styled-componentsなどがメジャーどころです。現代的な開発では、ReactやVueといったフロントエンド用のViewライブラリを使う際にCSS in JSのお世話になることがよくあります。

この記事では、次の時代のCSS in JSはWeb Componentsベースのものになるのではないかという話をします。

Web Componentsの復習

Web ComponentsはいくつかのWeb標準の総称であり、特に重要なのはCustom ElementsShadow DOMです。いずれも、V1と呼ばれる仕様は全てのモダンブラウザでサポートされています(Safariが一歩遅れていて少し心配ですが)。

Custon Elementsは<x-foo>...</x-foo>のような独自の要素名を登録して、要素が作られた時の処理をどうこうしたりできる機能です。Shadow DOMはもっと重要なので、まだ知らない方向けに少し詳しく解説します。

Shadow DOM

Shadow DOMは要素の「内部実装」のようなものを定義できる機能です。例えば、次のdiv要素は中身にpがあるだけの単純な構造をしています。

<div id="area" style="border: 1px solid #666666">
  <p>Hello, world!</p>
</div>

div要素の表示

しかし、Shadow DOMをdiv要素にアタッチすることで、div要素に複雑な構造を与えることができます。

const div = document.getElementById('area');
// divにShadow Rootをアタッチする
const shadowRoot = div.attachShadow({ mode: 'open' });
// Shadow Rootの中身を書く
shadowRoot.innerHTML = `
<style>
  p {
    border: 1px solid red;
  }
</style>
<p>↓↓↓↓↓</p>
<slot></slot>
<p>↑↑↑↑↑</p>
`;

こうすると、div要素は次のように表示されます。

shadowRoot追加後のdiv要素の表示

つまり、div要素にShadow DOMをアタッチしたことで、それがdiv要素の中身として表示されるようになります。また、Shadow DOM内で<slot></slot>とありますが、この部分にdiv要素の子(今回は<p>Hello, world!</p>)が入ります。よって、div要素はあたかも次の中身を持っているかのような表示になります。

<div id="area" style="border: 1px solid #666666">
  <style>
    p {
      border: 1px solid red;
    }
  </style>
  <p>↓↓↓↓↓</p>
  <p>Hello, world!</p>
  <p>↑↑↑↑↑</p>
</div>

ただし、ここでShadow DOMの中にstyle要素が書かれていることは注目に値します。これはpに対するスタイル指定を持っていますが、Shadow DOM内に書かれたスタイルが影響力を持つのは同じShadow DOM内の要素に対してだけです。よって、同じShadow DOMに由来する<P>↓↓↓↓↓</p><p>↑↑↑↑↑</p>にはborder: 1px solid red;が適用されますが、<p>Hello, world!</p>はShadow DOMに由来しないのでborder: 1px solid red;が適用されません。

逆に、Shadow DOM外に次のようなスタイルを書いたとしても、Shadow DOM内には影響しません。

/* Shadow DOM外に書く */
p {
  color: blue;
  font-size: 2em;
}

スタイル追加後の表示

このように、Shadow DOMの中と外はスタイルが隔絶しています。CSS in JSを考えるにあたってはこれが一番重要です。

ViewライブラリとWeb Components

Web Componentsは、名前に「Components」とあることから分かるように、Web標準にコンポーネントの概念を持ち込む試みです。一方、我々はReactやVueなどのViewライブラリを通してコンポーネントという概念に慣れ親しんでいます。そのため、Web Componentsに対しては「ライブラリを使わなくてもコンポーネントを作れる」「ViewライブラリからWeb標準への以降」という視点がよく見られます。

しかし、筆者の考えは違います。そもそもViewライブラリはDOMというWeb標準に上に作られたライブラリであり、エコシステムです。であるならば、Web標準の進化はViewライブラリの衰退ではなく、むしろ進化に繋がるでしょう。ReactなどのViewライブラリのエコシステムも、Web Componentsを土台にしたものへと移行すると期待されます。これがこの記事の予言です。

そして、Web Componentsの恩恵を比較的受けやすいのがCSS in JSであると考えています。なぜなら、Shadow DOMがまさにそのための機能を提供してくれており、一方で既存のCSS in JSにはいまだに絶対的な唯一解が無く、進化の余地があると考えられるからです。

CSS in JSに係るエコシステムはWeb Componentsの恩恵を受けて新たな形に進化するでしょう。これがWeb Componentsを従えるということです。

「そうはいってもWeb ComponentsとかShadow DOMとか使ったことないし、学習コストが……あんな欠点やこんな欠点が……」といった考えをお持ちの方も多いでしょう。それは問題ありません。なぜならこの記事は次の時代の話をしているのであって、今すぐWeb Componentsを採用しろと言っているわけではないからです。

実際、Web Componentsはまだ進化の途中です。つい最近話題になったDeclarative Shadow DOMには筆者も期待しています。これがあれば、Web ComponentsベースのCSS in JSでSSRをうまいことできるからです(逆に言えば、これがないとWeb ComponentsのSSRが結構厳しいです)。

現在のCSS in JS

Web Components時代のCSS in JSの話をする前に、現在のCSS in JSをちょっと見てみましょう。

CSS in JSをやることに悩ましいのは、スタイルをどうローカル化するかです。現在のベストプラクティスについては、筆者の意見は次の記事に近いものです(React + styled-componentsが前提)。

1番目の記事からDOM層Style層のコンポーネント例を引用します。

// DOM層
const Component: React.FC<​Props> = props => (
  <div className={props.className}>
    <button onClick={props.handleClick}>
      {props.flag ? 'click me' : 'CLICK ME'}
    </button>
  </div>
)

// Style層
const StyledComponent = styled(Component)`
> button {...}
`

すなわち、DOM構造のみを担当するコンポーネントを用意し、そのDOM構造に対してStyle層でスタイルを付けるというやり方です。この方法の利点としては、DOMの構造に対してスタイルを一括して書くことができる点です。つまり、上の例で言えば、Componentが定義するdivbuttonのスタイルは両方ともStyledComponentsにまとまっています。CSSは、display: flexなどに代表されるように、親要素と子要素のスタイルが協調しながらデザインを実現することがあります。それに対応する自然な形は、このように単体の要素ごとではなくある程度の大きさのDOM構造に対してまとめてスタイルを書く形でしょう。

ちなみに、筆者はこちらの方が好みです(classNameベースのデータ受け渡しが煩わしいので)。これはこれで、タグ名が一箇所だけ別の場所に行ってしまうという欠点を持っているのですが。

// DOM層
const Component: React.FC<​Props> = props => (
  <Wrapper>
    <button onClick={props.handleClick}>
      {props.flag ? 'click me' : 'CLICK ME'}
    </button>
  </Wrapper>
)

// Style層
const Wrapper = styled.div`
  & > button {...}
`

ただ、筆者の考えでは、これらはまだ完成形にたどり着いていません。その理由は主に2つあります。

まず、本来一体のものである「DOM構造」と「それに対するスタイル」を2層に分けて記述しなければいけないこと。そして次に、スタイルの漏洩に気をつけないといけないことです。スタイルの漏洩については、先の記事に次のような記述があります。

>による、children への指定漏洩防御も忘れない様にします。

つまり、先の例で& > buttonと書いてあるところをうっかり& buttonのようにしてしまったら、Wrapperが関知しない部分(外から子要素として与えられた部分)のbuttonにもスタイルが当たってしまうので、それをしないように気をつけなければならないということです。

この2つの問題が、Web Components時代には解決されます。

Web Components時代のCSS in JS

Shadow DOMを使えば、先ほどのコンポーネントはおおよそ次のような構成になるでしょう。まず、ComponentコンポーネントのShadow DOM内に次のHTMLを入れます。

<style>
button {
  ...
}
</style>
<div>
  <button>
    <slot></slot>
  </button>
</div>

これを使う側はこんな感じで使います。元のコンポーネントとロジックの位置がちょっと違っていますが、ロジックはスタイルから完全分離した方がいい(Shadow DOMベースのやり方の場合はそちらの方が向いている)ためこの形になっています。

<Component>{
  flag ? 'click me' : 'CLICK ME'
}</Component>

こうすると、「HTML構造」と「それに対するスタイル付け」をまとめてShadow DOMの中に放り込むことができ、両者を別々に書かなければならなかった問題が解消します。また、スタイルの漏洩に関しても、Shadow DOMならば鉄壁です。最も簡潔な記述ながら最も堅牢です。それどころか、もっと複雑なスタイルならば、クラスも使い放題です。

<style>
.submitButton {
  ...
}
</style>
<div>
  <button class="submitButton">
    <slot></slot>
  </button>
</div>

クラス名が他と被ることなんて心配する必要はありません。なぜなら、Shadow DOMの外に.submitButton { ... }というスタイルがあったとしても、Shadow DOMの中には影響を与えられないからです[1]。なんという理想郷でしょう。

ただ、まだ筆者が考えられていない点もあります。元のコンポーネントにあったonClickが上のコンポーネントからは消えていますね。CSS in JSという文脈でイベントをどう扱うのが最も自然かについてはまだいまいち答えを出せていません。考えている途中です。まあ未来の話ですから、その未来が来るまでに考えておけば大丈夫ですね。

react-wc

実は、上記の考え方をもとにすでにreact-wcというライブラリを作ってしまいました。

一定の使い道ならばすでに利用可能ですが、どちらかといえばまさに“未来”を担う次世代のCSS in JSライブラリを目指しています。詳しいことは当該のブログ記事を見ていただきたいのですが、CSS in JSの用途には次のように使用可能です。Shadow DOMに突っ込みたい物を文字列で指定するだけというとても単純な仕様です。Shadow DOM内はアップデートされることを想定しておらず、そのようなものはslot()<slot></slot>に相当)で渡す必要があります。

import { html, slot } from "react-wc";
// Reactコンポーネントが作られる
const Component = html`
  <style>
  .submitButton {
    ...
  }
  </style>
  <div>
    <button class="submitButton">
      ${slot()}
    </button>
  </div>
`;

// Reactコンポーネントとして使える
<Component>{flag ? "click me" : "CLICK ME"}</Component>

まとめ

この記事では、Web Componentsを前提としたCSS in JSの未来を予言しました。現状のCSS in JSの諸問題がShadow DOMによって解決されるのですから、準備さえ整えばその方向に進んでいくだろうというのが筆者の予言です。また、Web ComponentsはReactやVueを廃れさせるのではなく、(少なくともしばらくの間は)ViewライブラリがWeb Componentsを利用する形で進化していくだろうと予想しています。

今はまだWeb ComponentsやShadow DOMは目新しいと感じる人が多いかもしれませんが、これらはWeb標準の一部なのですから、言わば「生のDOM」と同じ立ち位置です。今はまだ……とWeb Componentsを避けていけば、その先にあるのは「React/Vueは使えるけど生のDOMは使えない人」という未来です(それがいいことなのか悪いことなのかは人によって意見が分かれるでしょうが、筆者は悪いと思っています)。

そろそろ、Web Componentsを見据えた次の時代について考え始めてもいいのではないでしょうか。筆者は考え始めたので先ほど紹介したreact-wcを作りました。今後もWeb Componentsの進化に合わせてreact-wcも進化していくでしょう。今のうちにreact-wcを褒めちぎったりcontributeしておいたりしたら次の時代の先駆者になれるかもしれませんよ。

脚注
  1. 一応、::partsを用いてShadow DOMの外のスタイルを中に適用することはできます。ただし、これは許可制です。つまり、Shadow DOMの内側から「この要素に外からスタイルを当てることを許可する」という宣言をする必要があります。 ↩︎