Shadow DOMを外部からスタイルできる新しい疑似要素「::part()」

本稿では、Shadow DOMの内部の要素のスタイルを、外側からスタイリングできるようにするCSS Shadow Partsについて紹介する。

## CSS Shadow Partsとは

* Shadowホスト(Shadow DOMを使う側のHTML)から`::part()`を定義することで、Shadow DOM内部の要素にスタイルを適用できる。
* 2018年11月15日にW3Cにドラフトが公開された仕様。
    * まだドラフトなので仕様変更があるかも知れない。
    * Chromeはバージョン73(2019年3月12日リリース)で対応されている。
* この仕様に従ったShadow DOMは、スタイルをユーザが調整できる。
    * 例えば、サードパーティ製のボタンコンポーネントがあって、サイトのメインカラーが緑だったとき、そのボタンコンポーネントの色もサイトのメインカラーに合わせることができる。

## 非推奨になった `::shadow` と `/deep/`

* Shadow DOMはDOMをカプセル化するための標準規格。
* `::shadow`セレクタと`/deep/`セレクタはShadow DOM内の要素をなんでも参照できてしまう仕様だった。
* そのため、カプセル化を破壊してしまうので非推奨になった。
* Chrome 63以降から`::shadow`と`/deep/`は削除されている[^chrome-remove-shadow-piercing]。

[^chrome-remove-shadow-piercing]: https://developers.google.com/web/updates/2017/10/remove-shadow-piercing

## CSS Shadow Partsと`::shadow`のアプローチの違い

* `::shadow`: Shadow DOM内部の要素を何でもスタイリングできる。
    * プライベートAPI無理くり叩くようなアプローチ。
    * コンポーネント作者は、どんなセレクタで内部要素に依存されるか予想できないという問題があった。
    * コンポーネント利用者は、依存したセレクタがいつどう変更されるか分からず不安という問題があった。
* CSS Shadow Parts: 明示的に公開されたShadow DOM内部要素のみスタイリングできる。
    * インターフェイスやパブリックメソッドを定義し、外部からはそれだけが使えるようにするのと似たアプローチ。
    * 公開する要素には`part`属性で名付けする。例: `<button part="mybutton">`
    * コンポーネント作者は、どこを公開できるかコントロールできる。(同時に何が公開されていて、スタイリングできるかはドキュメンテーションが大事になりそう)
    * コンポーネント利用者は、公開されたIDでアクセスできるので安心できるし、複雑なセレクターを書かなくて良くなる。

また、CSS Shadow Parts以外の外部からコンポーネントのスタイルを調整する手法として

* [CSS Custom Propertiesを使った方法](https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=ja#stylefromoutside)
* Shadow DOM内で外部CSSファイルを読み込む方法

があるが、本稿では割愛する。

## CSS Shadow Partsを使ったサンプルコード

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="css,result" data-user="suin" data-slug-hash="QXgRXb" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="CSS Shadow Parts">
  <span>See the Pen <a href="https://codepen.io/suin/pen/QXgRXb/">
  CSS Shadow Parts</a> by suin (<a href="https://codepen.io/suin">@suin</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

```index.html
<link href="style.css" rel="stylesheet">
<script src="main.js"></script>

<h3>ホスト側の見出し3</h3>
<my-h3>シャドーDOM内の見出し3</my-h3>
<my-h3-no-part>シャドーDOM内の見出し3(partなし)</my-h3-no-part>
```

```main.js
class MyH3 extends HTMLElement {
  constructor() {
    super();
    const node = this.attachShadow({ mode: "open" });
    node.innerHTML = `<h3 part="title"><slot/></h3>`;
  }
}

class MyH3NoPart extends HTMLElement {
  constructor() {
    super();
    const node = this.attachShadow({ mode: "open" });
    node.innerHTML = `<h3><slot/></h3>`; // この要素は外部からスタイリングできない
  }
}
customElements.define("my-h3", MyH3);
customElements.define("my-h3-no-part", MyH3NoPart);
```

```style.css
h3 {
  color: blue;
}

/* my-h3はカスタム要素名、titleはmy-h3内部要素<h3>のpart属性を指す */
my-h3::part(title) {
  color: green;
}
```

実行結果

<img width="387" alt="CSS_Shadow_Parts.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/98194b80-7201-9b58-5f41-d4255ba22373.png">



## 参考

* [CSS Shadow Parts](https://drafts.csswg.org/css-shadow-parts-1/)
* [CSS Shadow Parts(邦訳)](https://triple-underscore.github.io/css-shadow-parts-ja.html)
* [CSS Shadow Parts - YouTube](https://www.youtube.com/watch?v=yVrvOueeb3s)
* [::part and ::theme, an ::explainer – Monica Dinculescu](https://meowni.ca/posts/part-theme-explainer/)