ベーシック アドベントカレンダー 2日目です。

Shadow DOM でコンポーネントに閉じ込めた CSS を定義することができますが、CSS の定義がやりやすいということはないです。<style> タグを JS 内でベタで書く、もしくは JS で style API を直接修正する…。ちょっとしたコンポーネントであれば良いですが現実的ではありません。

通常の HTML / CSS と同様に外部ファイルで管理したくなります。現時点では3つの方法が提供されています。

今回のデモとソースコードはこちら

<link rel="stylesheets">

普通に <link rel="stylesheets"> で外部 CSS ファイルを読み込むことができます。以前はできなかったようですが現在は標準化されるようです

class NormalLinkCss extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
  }

  connectedCallback() {
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="sample.css">
<p>Normal Import CSS</p>
`
  }
}

customElements.define('normal-link-css', NormalLinkCss)

@import

<link rel="stylesheets"> と同様に @import も利用できます。

class NormalImportCss extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
  }

  connectedCallback() {
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = `
<style>
  @import "sample.css"; 
</style>
<p>Normal Import CSS</p>
`
  }
}

customElements.define('normal-import-css', NormalImportCss)

adoptedStyleSheets

上記2つのやり方とは違うやり方で CSS を共通化して利用することができます。今後はこちらのやり方が主流になりそうです。

const css = new CSSStyleSheet()
css.replaceSync(`
p {
  color: #00f;
}
`)

class AdoptedCss extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.shadowRoot.adoptedStyleSheets = [css]
  }

  connectedCallback() {
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = `<p>Adopted CSS</p>`
  }
}

customElements.define('adopted-css', AdoptedCss)

new CSSStyleSheet() で CSS オブジェクトを生成し、それを ShadowRoot に読み込ませることでスタイルを適用できます。adoptedStyleSheets は配列でいくつでも CSS オブジェクトを追加できます。

この adoptedStyleSheets は何が違うのかと言うと、 <link rel="stylesheets">@import の場合は ShadowRoot の中にスタイル定義が個別に存在しますが、 adoptedStyleSheets はユーザスタイルシートのように全体に定義されているように見えます(ここらへんは詳しくは分からず推測です)。

これによって、わずかですがレンダリングのパフォーマンスが向上しているのが確認できました。が、簡単なスタイルだと体感することはないレベルの差でした。ブラウザ内部での共通 DOM のスタイル定義を最適化している処理とかもあると思うので、プロダクションレベルでどうなるかはちょっと分からないです。ただ、この機能が実装された背景の一部がパフォーマンスだったので期待はできそうです。

adoptedStyleSheet() でも外部 CSS ファイルを使えます。

const css = new CSSStyleSheet()
css.replace(`
@import 'sample.css';
`)

class AdoptedImportCss extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.shadowRoot.adoptedStyleSheets = [css]
  }

  connectedCallback() {
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = `<p>Adopted import CSS</p>`
  }
}

customElements.define('adopted-import-css', AdoptedImportCss)

ただし @import を使った場合は replaceSync() が使えません。replace の非同期にする必要があります。これは FOUC を引き起こす可能性があります(<link rel="stylesheets">@import を使っている場合は起こる)。

FOUC を避ける

FOUC ( Flash of Unstyled Content ) とは、ページを読み込んだ際に CSS があたっていない状態の DOM が一瞬見えてしまうことです。CSS ファイルを読み込んだタイミングによっては、ちらつきが発生してしまい、あまり気持ちのいい状態ではないです。

Web Components でも外部 CSS ファイルを使った場合に FOUC を避けたい気持ちがあるので、避けるように試してみます。

replace() は Promise を返すので、CSS ファイルの読み込みが完了してから DOM を構築するようにするだけです。

class AdoptedImportCss extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
  }

  connectedCallback() {
    this.loadCss()
  }

  loadCss() {
    const css = new CSSStyleSheet()
    css.replace(`@import 'sample.css';`).then(() => {
      this.render()
    })
    this.shadowRoot.adoptedStyleSheets = [css]
  }

  render() {
    this.shadowRoot.innerHTML = `<p>Adopted import CSS</p>`
  }
}

customElements.define('adopted-import-css', AdoptedImportCss)

これで FOUC を避けることができます。

というわけで、Web Components Shadow DOM で CSS を管理する方法を紹介しました。