ベーシック アドベントカレンダー 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 を管理する方法を紹介しました。