【テクニカル・上級編】 :defined 定義済み疑似クラス – CSS実践ガイド

モダンなWebフロントエンドのアーキテクチャを設計する際、我々が常に戦わなければならないのは「不確実な時間」だ。JavaScriptがロードされ、実行され、DOMが「生命」を宿すまでの空白期間。このわずかなギャップに、ユーザー体験の質を左右する巨大な落とし穴が潜んでいる。

今日は、Web Components(Custom Elements)における最後のミッシングピースとも言える疑似クラス、`:defined` について、その深淵を覗いてみよう。

—

状態の不在をデザインする:`:defined` の本質

我々が `` のようなカスタム要素をHTMLに記述した瞬間、ブラウザのパーサーはそれを「未知の要素」として扱う。この時点では、その要素はただの `HTMLElement` のインスタンスに過ぎない。その後、JavaScriptがロードされ `customElements.define()` が呼び出されて初めて、その要素は真のアイデンティティを得る。

この「定義待ち」の状態をCSSから正確に捕捉できる唯一の手段が `:defined` 疑似クラスだ。

多くの開発者が犯す過ちは、JavaScript側で `opacity: 0` などのクラスを付け外ししてローディングを制御しようとすることだ。しかし、それでは遅すぎる。スクリプトが実行される前の「生のHTML」が描画される瞬間に、レイアウトの崩壊(Layout Shift)は既に始まっているからだ。

レイアウト・ジャンプという「静かな毒」

未定義のカスタム要素は、デフォルトで `display: inline` として振る舞う。もしあなたのコンポーネントが複雑なグリッドレイアウトを構成する予定なら、定義される前と後で要素のボックスモデルが劇的に変化し、壊滅的な累積レイアウトシフト(CLS)を引き起こすだろう。

`:defined` を使った堅牢なアーキテクチャの基本は、「未定義状態をデフォルトとし、定義済み状態をゴールとする」ことにある。

/ まだブラウザに認識されていないカスタム要素へのアプローチ /
my-dashboard-card:not(:defined) {
/

  • 重要: レイアウトの予約。
  • コンポーネントがロードされる前の「ガワ」を確保し、
  • 後続の要素が押し上げられるのを防ぐ。

/
display: block;
min-height: 300px;
background-color: #f0f0f0;
border-radius: 8px;
position: relative;
overflow: hidden;
}

/ スケルトン・スクリーンのアニメーションを付与 /
my-dashboard-card:not(:defined)::after {
content: “”;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: skeleton-shimmer 1.5s infinite;
}

@keyframes skeleton-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}

/ 定義が完了した瞬間にスタイルを切り替える /
my-dashboard-card:defined {
/ 定義後は通常のコンポーネントとして振る舞う /
animation: fade-in 0.3s ease-out;
}

@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

レンダリング負荷とメモリ効率の力学

`:defined` のセレクタ評価は、ブラウザのレンダリングエンジン(BlinkやWebKit)にとって非常に軽量な処理だ。なぜなら、要素の内部状態(プロパティ)を監視するのではなく、要素の「登録簿」との照合だけで済むからだ。

しかし、大規模なシングルページアプリケーション(SPA)において、数千のカスタム要素が動的に追加・削除されるケースでは、アーキテクチャ上の注意が必要になる。

1. Style Recalculationのコスト: `customElements.define()` が実行された瞬間、ブラウザはそのタグ名を持つ全ての要素に対してスタイルの再計算を行う。`:defined` セレクタが複雑な子孫結合子(例: `:not(:defined) > .child .grandchild`)を含んでいる場合、この再計算のコストは線形に増大する。可能な限り、`:defined` 自体に直接スタイルを当てるか、CSS変数の切り替えに留めるのがスマートだ。

2. 非同期ロードの競合: 複数のコンポーネントが異なるタイミングで定義される場合、ページ全体のレイアウトが「ガタガタ」と段階的に動く可能性がある。これを防ぐには、コンポーネントの粒度を適切に設計するか、あるいはクリティカル・ビューに含まれるコンポーネント群を `Promise.all([customElements.whenDefined(‘…’), …])` で一括制御する戦略が有効だ。

重大なバグの回避策:フォールバック・デザイン

我々プロフェッショナルが考慮すべき最悪のシナリオは、「JavaScriptがロードされなかった、あるいはエラーで停止した」場合だ。

`:defined` を使わずに「JSでクラスを付与して表示する」設計にしていると、JSが死んだ瞬間にそのコンテンツは永遠に不可視(`display: none` や `opacity: 0`)となる。これはアクセシビリティとレジリエンス(回復力)の観点から見て、敗北を意味する。

`:defined` を活用した「プログレッシブ・エンハンスメント」の考え方を取り入れよう。

/

  • JSが動かなくても、最低限のコンテンツは見せる。
  • カスタム要素内の「フォールバックHTML」をどう扱うか。

/
my-video-player:not(:defined) {
/

  • JSが動かない環境やロード中であっても、
  • 内部の標準的な

/
display: block;
padding: 20px;
border: 1px dashed #ccc;
}

/

  • 定義されたら、内部のフォールバック用要素を隠し、
  • Shadow DOMによる高度なUIに切り替わるのを待つ。

/
my-video-player:defined > .fallback-content {
display: none;
}

チーフアーキテクトの視点:次世代のコンポーネント設計

Web Componentsは、もはや単なる「カプセル化の技術」ではない。DOMのライフサイクルそのものをCSSのセレクタレベルで制御可能にする、ブラウザネイティブの強力なプリミティブだ。

`:defined` を使いこなすことは、ブラウザのパース・フェーズからハイドレーション・フェーズに至るまでの「時間の流れ」をデザインすることに他ならない。

  • パフォーマンス最適化: `:not(:defined)` 時に重いフィルタや複雑な計算を避け、レイアウトの安定性(Intrinsic Sizeの確保)に集中せよ。
  • UXの向上: ユーザーに「壊れたページ」を見せるな。「準備中のページ」を見せろ。
  • デバッグ: ブラウザのDevToolsで、コンポーネントが `HTMLUnknownElement` になっていないか、`:defined` がいつ適用されたかをネットワークプロファイルと照らし合わせて検証せよ。

我々が作っているのは、ただのWebサイトではない。あらゆるネットワーク環境、あらゆるデバイスで、揺るぎなく動作する「堅牢なソフトウェア」だ。そのための第一歩が、この `:defined` という静かな、しかし力強い疑似クラスの理解にある。

次にカスタム要素を設計する時は、その要素が「定義される前の姿」に、定義された後の姿と同じくらいの情熱を注いでみてほしい。それが、一流と二流を分ける境界線なのだから。

コメント

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