【テクニカル・上級編】 :defined擬似クラス – CSS実践ガイド

孤高の番人、`:defined`:未定義カスタム要素の「閃光のチラつき(FOUC)」をねじ伏せるアーキテクチャ

フロントエンドの荒波を越えてきたシニアアーキテクト諸君なら、一度は頭を抱えたことがあるはずだ。そう、Web Componentsの「カスタム要素(Custom Elements)」を導入した瞬間に発生する、あの残酷なまでの「未定義の瞬間の露出(FOUC: Flash of Undefined Custom Elements)」に。

JavaScriptのバンドルが読み込まれ、`customElements.define()` が実行されるまでのわずか数十ミリ秒。DOM上には、まだスタイルも当たっていない、ただの無機質なタグ名が剥き出しで描画される。あの目障りなレイアウトシフトとデザインの崩れを、君はこれまでどうやっていなしてきた?

「親要素に隠しクラスをつけておいて、JSで剥がす」?
「`visibility: hidden` をごり押しする」?

……おいおい、それじゃあまるで素人の力技だ。モダンブラウザのレンダリングエンジンは、もっと洗練された武器を用意してくれている。それが今回スポットを当てる `:defined` 擬似クラス だ。

この一見地味なセレクタの裏側には、ブラウザのパース処理、非同期のライフサイクル、そしてレンダリング最適化の深遠な知見が詰まっている。今回は、この `:defined` を極限まで使い倒し、堅牢でシームレスなWebアプリケーションを構築するための実践的アーキテクチャを紐解いていこう。

—

1. なぜ `:defined` なのか? ブラウザエンジンの内部挙動から紐解く存在意義

まず、`:defined` が何にマッチするのかを正確に定義しておこう。
`:defined` は、「すでにブラウザに定義が登録されているカスタム要素」、あるいは「そもそもカスタム要素ではないすべての標準HTML要素(`div`, `span`, `button` など)」にマッチする。

逆に言えば、カスタム要素でありながら、まだ `customElements.define()` が完了していない、つまりJavaScriptの実行待ちの状態にある要素だけにはマッチしない。

ここでBlinkやGeckoなどのブラウザエンジンの内部挙動を思い出してほしい。ブラウザはHTMLをパースする際、未知のタグに出会うと「不明な要素(HTMLUnknownElement)」として扱い、インライン要素としてのデフォルトスタイルを適用して描画を進める。その後、JavaScriptによってコンストラクタが登録され、アップグレード(Upgrade)プロセスを経ることで、ようやく真のカスタム要素として生を受ける。

この「パースからアップグレードまでのタイムラグ」が、FOUCの正体だ。

従来のハックでは、このタイムラグを隠すためにJavaScriptのロード完了を待ったり、DOM全体を強引に隠蔽したりしていた。しかし、これではメモリ効率が悪化し、First Contentful Paint (FCP) や Largest Contentful Paint (LCP) のメトリクスに悪影響を及ぼす。

`:defined` を使えば、JavaScriptの介入なしに、CSSの宣言的レイヤーだけでこのタイムラグを完全に制御できる。これこそが、アーキテクチャの観点から見た最大の美徳だ。

—

2. 実践的アーキテクチャ:FOUCを撲滅する `:not(:defined)` パターン

では、実務でどう使うのか。最もエレガントかつ破壊力のあるイディオムは、`:not(:not(:defined))`……いや、シンプルに `:not(:defined)` を用いたプレースホルダー戦略だ。

以下のコードを見てほしい。大規模なデザインシステムにおいて、カスタムボタン(``)の未定義時の挙動を制御する実例だ。

/ ==========================================================================
Custom Elements: Defined State Architecture
========================================================================== /

/ 1. まだ定義されていないカスタム要素のベーススタイル(隠蔽とスケルトン) /
my-button:not(:defined),
my-card:not(:defined),
my-dialog:not(:defined) {
/ レンダリングの暴走を防ぎつつ、レイアウトシフト(CLS)を最小化する /
display: block;
visibility: hidden;
/ 必要に応じてアニメーションするプレースホルダーの高さを確保 /
min-height: 44px;
contain: content; / ブラウザのレンダリング負荷を劇的に下げる孤立化 /
}

/ 2. 定義が完了した(アップグレードされた)瞬間に適用されるトランジション /
my-button,
my-card,
my-dialog {
/ opacityを変化させて、ポップインのような不自然さを排除し滑らかに顕現させる /
opacity: 1;
visibility: visible;
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
contain: layout style paint;
}

/ 3. 定義前の初期状態ではフェードアウト(あるいは非表示)を維持 /
my-button:not(:defined) {
opacity: 0;
}

このアプローチの極めて優れている点は、CSS Containment(`contain: content` や `contain: layout style paint`)とのシナジーにある。
未定義のカスタム要素に対してあらかじめ `contain` プロパティを付与しておくことで、万が一スタイルが適用される前に子孫要素の再描画が発生しても、ブラウザのレイアウトツリー全体への影響を極小化し、レンダリングエンジンの負荷を劇的に軽減できるのだ。メモリ効率とパフォーマンスのチューニングにおいて、これはプロフェッショナルなら常識として押さえておきたいテクニックだ。

—

3. 高度な応用:非同期競合とフォールバックの設計思想

モダンなWebアプリケーションでは、コード分割(Code Splitting)や遅延ロード(Lazy Loading)が当たり前に行われている。ユーザーが特定のルートに遷移した瞬間に、そのページで必要なカスタム要素の定義スクリプトが非同期でフェッチされる。

ここで問題になるのが、「ネットワークの遅延によるカスタム要素の定義遅延」と「SSR(Server-Side Rendering)環境との不整合」だ。

SSRを行う場合、サーバー側ではHTML文字列として出力されるため、`:defined` の判定がクライアントのハイドレーション(Hydration)プロセスと密接に絡み合う。もしハイドレーションの完了前に `:not(:defined)` によるアグレッシブな隠蔽を行いすぎると、ユーザービリティを損なう原因になりかねない。

ここで、カスタム要素の内部にフォールバックコンテンツを仕込み、`:defined` と組み合わせる高度なスロット戦略を見てみよう。



データを読み込んでいます…

/ ==========================================================================
Advanced Fallback Architecture with :defined
========================================================================== /

/ カスタム要素が未定義の間は、内部のフォールバック用スロット/要素を強制表示 /
my-data-grid:not(:defined) .fallback-skeleton {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: var(–color-surface-muted, #f3f4f6);
border-radius: 8px;
}

/ カスタム要素が定義された(アップグレードされた)瞬間、フォールバックを完全に消滅させる /
my-data-grid:defined .fallback-skeleton {
display: none;
}

/ 未定義時のカスタム要素自体のボックスモデルを安定させる /
my-data-grid:not(:defined) {
display: block;
width: 100%;
min-height: 200px;
}

このパターンの何が美しいか? JavaScriptのイベントリスナーやMutationObserverを一切書かずに、CSS単体で「未定義状態から定義済み状態へのスイッチング」を宣言的に完結させている点だ。 メインスレッドのJavaScript実行キューを一切汚染せず、パフォーマンスへの負荷をゼロに抑えながら、極めて堅牢なUIフォールバックを実現している。

—

4. チーフアーキテクトからの警告:実務で踏みがちなしこと

さて、ここまで `:defined` の圧倒的なポテンシャルを語ってきたが、現場の泥臭い実務において、シニアエンジニアが陥りがちな「罠」についても言及しておかねばならない。

罠その1:Shadow DOMの外部からのセレクタ詳細度とカプセル化の壁

カスタム要素の内部(Shadow DOM)のスタイルは原則として外部のCSSからはカプセル化されている。したがって、`:defined` を適用できるのは、あくまで「カスタム要素そのもの(ホスト要素)」に対してのみだ。
「カスタム要素の内部にある子要素の定義状態を `:defined` で制御しよう」なんていう愚行を犯してはならない。Shadow DOMの境界を越えることはできない。内部の要素の制御は、あくまでそのコンポーネント内のコンポーネント自身(あるいはShadow CSS)の責務である。

罠その2:CSSのカスケーディング順序の誤認

`:defined` は擬似クラスであるため、セレクタの詳細度(Specificity)においては通常の一致クラスや疑似クラス(`:hover` や `:focus` など)と同等に扱われる。
そのため、次のような汎用的なセレクタに負けてしまうことがある。

/ このような大雑把なセレクタが後から読み込まれると、:defined の制御が上書きされる /
body {
display: inline; / 悲劇の始まり /
}

これを防ぐためには、CSSアーキテクチャのレイヤー(ITCSSやBEM、あるいはCascade Layers (`@layer`))を適切に設計し、`:defined` を用いた状態管理ルールを明確なレイヤーに閉じ込める必要がある。

@layer components {
/ コンポーネントの状態レイヤーとして厳格に管理する /
my-widget:not(:defined) {
display: block;
visibility: hidden;
}
}

—

5. 結び:細部に宿るプロフェッショナリズム

フレームワークがどれほど進化し、ビルドツールがどれほど高速化しようとも、ブラウザのレンダリングパイプラインの根底にある「HTMLのパースからDOMの構築、そしてスクリプトの実行とアップグレード」という一連の物理法則が変わることはない。

そのタイムラグの狭間に生まれるわずかなノイズ(FOUC)を、JavaScriptの力技でねじ伏せるのではなく、ブラウザの仕様そのものである `:defined` というセマンティックなセレクタで優雅にいなす。それこそが、私たちフロントエンド・スペシャリストが目指すべき、美しく堅牢なアーキテクチャの姿だ。

コードエディタを開け。今すぐ、君のプロジェクトに散らばる場当たり的なFOUC対策の残骸を削除し、`:defined` による真の宣言的スタイリングへと書き換え给う。

ブラウザは、いつだって正しいセマンティクスを知っている。あとは、私たちがそれを信じてコードに落とし込むだけだ。

コメント

タイトルとURLをコピーしました