0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Shadow DOMの向こう側は、支援技術からどう見えるのか?

0
Posted at

はじめに: Shadow DOMの向こうに、意味は届くのか

前回の記事「aria-labelを書いたあと、何が起こっている?」では、HTML から DOM(Document Object Model)、Accessibility Tree(アクセシビリティツリー)へ続く経路を追いました。さらに Platform Accessibility API(プラットフォームのアクセシビリティ API)を経て意味が届く流れも扱いました。今回は、その経路に Shadow DOM が加わるとどう見えるかを考えます。

HTML / UI Framework
        ↓
DOM
        ↓
Accessibility Tree
        ↓
Platform Accessibility API
        ↓
支援技術
        ↓
ユーザー

これは情報伝達を理解するための概念モデルです。WAI-ARIA はアクセシビリティ上の意味を定義し、HTML-AAM と Core-AAM はその意味をプラットフォーム API に対応づけますが、ブラウザー内部に共通の単一ツリーがあるという意味ではありません。WAI-ARIA 1.2 HTML-AAM 1.0 Core-AAM 1.2

Web Components を設計・利用する開発者向けに、Shadow DOM がカプセル化する範囲を整理します。また、仕様と Chrome での限定的な観察を通じて、ブラウザーがアクセシビリティ情報を公開する仕組みを見ていきます。今回はスクリーンリーダーによる読み上げ比較ではなく、ブラウザーが公開する情報とキーボードフォーカスを確認します。

出発点は、次の Custom Element です。

<my-button></my-button>

内部にネイティブの<button>があるとしたら、Light DOM から見えないボタンを支援技術は認識できるのでしょうか。open と closed は公開範囲に関わるのでしょうか。

Shadow DOM をアクセシブル/非アクセシブルの二択にするのではなく、DOM のカプセル化と UI の意味を公開する仕組みを、仕様・Chrome 観察・設計の層に分けて考えます。

ここで確かめたいのは、Light DOM から直接見えない内部ボタンがどう扱われるか、slot のテキストがその名前に関係するか、open/closed が何を制御するかです。さらに、意味を持つのが host 自身か内部のネイティブ要素か、そしてフォーカスやイベント、要素間の関係が境界を越えてどう働くかを見ていきます。

Shadow DOM は何をカプセル化するのか

Shadow DOM は要素(Shadow Host)に別の DOM ツリー(Shadow Tree)を関連付ける仕組みです。内側の根が Shadow Root、外側の通常の DOM が Light DOM です。DOM Standard - Shadow Trees MDN: Using shadow DOM

<my-button></my-button>

<script>
  class MyButton extends HTMLElement {
    constructor() {
      super();

      const shadowRoot = this.attachShadow({ mode: "open" });
      const button = document.createElement("button");
      button.textContent = "保存";
      shadowRoot.append(button);
    }
  }

  customElements.define("my-button", MyButton);
</script>
Light DOM
    ↓
Shadow Host
    ↓
Shadow Root
    ↓
Shadow Tree
    ↓
Accessibility Treeではどうなる?

Shadow Boundary は、DOM 操作や CSS スコープなど実装構造をカプセル化します。open なら host.shadowRoot から root を取得できますが、closed では通常 null です。これは JavaScript からの参照範囲の違いで、アクセシビリティ情報の公開設定ではありません。DOM Standard - Shadow Trees

つまり、カプセル化から支援技術への非公開を導くことも、内部が必ず公開されると結論づけることもできません。

DOM ツリーと Accessibility Tree は同じものではない

DOM、slot による表示構造、Accessibility Tree は同じツリーではありません。DOM はノードの親子関係を表します。CSS Shadow Module が定義する flattened element tree(平坦化された要素ツリー)は、Shadow Tree と slot 割り当てを CSS のために考慮する構造です。一方、Accessible Name and Description Computation は名前計算のために Shadow Root と slot を考慮した「rendered child nodes」を定義します。文脈によって使われる composed tree も含め、これらを一つの同じツリーとみなすことはできません。DOM Standard - Shadow Trees CSS Shadow Module Level 1 - Shadow Encapsulation Accessible Name and Description Computation 1.2

Accessibility Tree はさらに別のモデルで、UI の名前・役割・状態・プロパティなどの意味を扱います。したがって、composed tree がそのまま Accessibility Tree になるとも、DOM と一対一に対応するとも言えません。WAI-ARIA 1.2

Shadow DOM
  → 実装構造をカプセル化する

ブラウザー
  → HTML、ARIA、Shadow Tree、slot割り当てなどを解釈する

アクセシビリティ情報
  → UIの名前・役割・状態・関係を公開する

図は概念モデルです。「合成ツリー(composed tree)がそのまま Accessibility Tree になる」とは言えず、DOM との一対一対応もありません。Chromium 資料も両者の構造が同一でないと説明しますが、他エンジンの保証ではありません。Chromium: How Chrome Accessibility Works

WAI-ARIA は role/state/property の意味を定義します。HTML-AAM と Core-AAM は HTML/ARIA から Platform Accessibility API への対応を扱います。2026-10-20 に確認した時点で、WAI-ARIA 1.2 は Recommendation、HTML-AAM 1.0 は Working Draft、Core-AAM 1.2 は 2026-10-12 付の Candidate Recommendation Draft です。Core-AAM の最新 Recommendation は 1.1 です。WAI-ARIA 1.2 HTML-AAM 1.0 Core-AAM 1.2

<my-button> という名前だけでは、ネイティブの<button>と同じ意味は得られません。host 自身に意味を持たせるか、内部のネイティブ要素に持たせるかを選びます。HTML Standard: Drawbacks of autonomous custom elements

実験: Shadow Root、slot、ElementInternals を同じページで見る

実験ページの準備

次はネイティブボタン、open/closed root、slot、fallback、ElementInternals を含む単体 HTML です。ElementInternals は、Custom Element 自身に既定の role や ARIA プロパティを与える仕組みです。保存して DevTools とキーボードで観察できます。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Shadow DOM accessibility experiment</title>
</head>
<body>
  <h1>Shadow DOMのアクセシビリティ観察</h1>
  <button type="button">Native の保存</button>
  <my-open-button></my-open-button>
  <my-closed-button></my-closed-button>
  <my-slotted-button>Light DOM の保存</my-slotted-button>
  <my-fallback-button></my-fallback-button>
  <my-internals-button></my-internals-button>

  <script>
    const closedRoots = new WeakMap();

    function defineShadowButton(name, mode, markup) {
      customElements.define(name, class extends HTMLElement {
        constructor() {
          super();
          const root = this.attachShadow({ mode });
          root.innerHTML = markup;
          if (mode === "closed") closedRoots.set(this, root);
        }
      });
    }

    defineShadowButton(
      "my-open-button", "open",
      '<button type="button">Open root の保存</button>'
    );
    defineShadowButton(
      "my-closed-button", "closed",
      '<button type="button">Closed root の保存</button>'
    );
    defineShadowButton(
      "my-slotted-button", "open",
      '<button type="button"><slot></slot></button>'
    );
    defineShadowButton(
      "my-fallback-button", "open",
      '<button type="button"><slot>Fallback の保存</slot></button>'
    );

    class MyInternalsButton extends HTMLElement {
      constructor() {
        super();
        const internals = this.attachInternals();
        internals.role = "button";
        internals.ariaLabel = "ElementInternals の既定名";
      }
    }

    customElements.define("my-internals-button", MyInternalsButton);

    // 比較用テストハーネス。closed rootを製品コードから公開する例ではない。
    window.getClosedRootForTest = (host) => closedRoots.get(host);
  </script>
</body>
</html>

WeakMapとgetClosedRootForTestは比較用テストハーネスで、closed rootの実用アクセス方法ではありません。製品コードから参照できるようにすれば、カプセル化を保てません。

今回の Chrome 観察

検証日は 2026 年 10 月 19 日です。環境は Windows 11 Pro Insider Preview(version 26H2、OS build 26340.9616、Windows Feature Experience Pack 1000.26100.460.0)と Chrome 155.0.0.0 です。スクリーンリーダーと他ブラウザーは未確認です。

Elements パネルで対象要素を選び、Accessibility パネルの Show accessibility tree でツリーを確認できます。フォーカスを各ボタンへ移してから Console で次を評価すると、document と Shadow Root それぞれの active element を比較できます。closed root の例だけは、サンプルのテスト用関数を使います。Chrome DevTools: Accessibility reference MDN: Document.activeElement MDN: ShadowRoot.activeElement

document.activeElement
document.activeElement.shadowRoot?.activeElement
getClosedRootForTest(document.querySelector("my-closed-button"))?.activeElement

以下は Accessibility Tree snapshot とキーボードによる、このページを Chrome 155 で観察した結果です。読み上げや他ブラウザーへ一般化しません。DevTools はブラウザーの情報を観察するもので、支援技術による読み上げではありません。Chrome DevTools: Accessibility reference

パターン Chrome 155 の snapshot で観察した内容
Native button button、名前は「Native の保存」
open root host は generic、内部のbuttonとテキスト名が表示
closed root host は generic、内部のbuttonとテキスト名が表示
slot あり 内部ボタンの名前は「Light DOM の保存」
fallback 内部ボタンの名前は「Fallback の保存」
ElementInternals host 自身がbutton、名前は「ElementInternals の既定名」。host のroleとaria-label content attribute はどちらもnull

今回の記録は snapshot とキーボードに限り、DOM パネルの具体的な表示結果は含みません。Accessibility Tree snapshot はブラウザーが公開する情報の観察であり、支援技術による読み上げの確認ではありません。

Tab キーで各 button に移動した順は、Native → open root 内の button → closed root 内の button → slot 内の button → fallback 内の button でした。最後の button の後に Tab キーを押したとき、document.activeElement の観察値は body でした(body 自体を Tab 停止先とする意味ではありません)。Shadow DOM 内にフォーカスがあるとき、document.activeElement は host を返します。open root のshadowRoot.activeElementと、テスト用 WeakMap から取得した closed root のactiveElementは、それぞれ内側の button を返しました。MDN: Document.activeElement MDN: ShadowRoot.activeElement

ElementInternals の例は snapshot で button として表示されましたが、Tab 順には入りませんでした(tabIndex は -1)。意味付与だけでフォーカスや操作が実装されるわけではありません。

slotのテキストと名前計算

「Light DOM の保存」は host の Light DOM 子ノードです。slot への割り当てで DOM 上の親が<slot>に変わるわけではありません。割り当てる子がなければ fallback content が使われます。DOM Standard - Shadow Trees MDN: Using templates and slots

観察した名前は、割り当て済みテキスト/fallback text でした。AccName 1.2 の rendered child nodes 手順は Shadow Root の子をたどります。slot の assigned nodes がなければ、slot の子を参照します。これは名前計算であり、Accessibility Tree 全体の構造を定めるものではありません。同仕様は 2026 年 10 月 19 日時点で Working Draft です。Accessible Name and Description Computation 1.2

ElementInternalsでCustom Element自身に既定の意味を与える

HTMLElement.attachInternals()で得るElementInternalsから、roleやariaLabelなどの既定セマンティクスを設定できます。ページ作者はrole/aria-*属性で上書きできます。HTML Standard: Accessibility semantics MDN: ElementInternals

設計A: 内部のネイティブ要素が意味を持つ
Custom Element
  └─ Shadow DOM
       └─ <button>

設計B: Custom Element自身が既定の意味を持つ
Custom Element
  └─ ElementInternalsでroleやARIAプロパティを設定

ElementInternals は Accessibility Tree を直接組み立てる API ではありません。Accessibility Object Model(AOM)Explainer には abandoned/speculative な提案もあるため、AccessibleNode や virtual nodes を現行 API として扱いません。WICG: Accessibility Object Model Explainer

次は、意味が見えるだけでは解決しないフォーカスや操作へ視点を移します。

難しさは「見えるか」だけではない

ここからは今回の実験で確認した結果ではなく、Shadow DOM を使う際に別途設計・検証すべき論点を整理します。

サンプルではdelegatesFocusを使っていません。これは host への.focus()や非フォーカス領域のクリック時に内側へフォーカスを委譲する機能で、Tab 順序の万能制御ではありません。MDN: ShadowRoot.delegatesFocus

イベントのcomposed(境界を越えるか)とbubbles(親へ伝播するか)は別条件です。retargeting やcomposedPath()も境界越しの伝播に関係します。DOM Standard: Events MDN: Event.composed

ARIA の関係属性を Shadow Root 境界越しに使えるかは、属性・API ごとに確認が必要です。aria-labelledbyやaria-controlsの文字列 ID 参照と、ariaLabelledByElementsのような element reference API は別の仕組みです。後者には scope 条件があるため、別 root 間の関係を対象ブラウザーで検証します。WAI-ARIA 1.3: ariaLabelledByElements MDN: ElementInternals.ariaLabelledByElements

ダイアログ、メニュー、コンボボックス、データグリッドでは、フォーカス、イベント、関係属性、Portal/Overlay、状態更新通知が絡みます。snapshot だけでなく、キーボードと支援技術でも検証します。Chrome DevTools: Accessibility reference

「Shadow DOM はアクセシビリティを壊す」のか

Shadow DOM を使っただけで非アクセシブルになるとは限りませんが、自動で完成もしません。内部のネイティブ HTML、host の既定セマンティクス、名前・状態・フォーカス・関係・操作を検証します。HTML Standard: Accessibility semantics WAI-ARIA 1.2

open/closed の参照可否だけで公開範囲は判断できません。仕様・ブラウザー実装・操作結果を分けて検証します。

この整理を UI ライブラリの設計に広げると、次の問いにつながります。

React と UI ライブラリの先へ

React と Web Components は別の仕組みです。React が DOM や Custom Element を生成した後も、ブラウザーがアクセシビリティ情報を計算する経路は続きます。React: React DOM Components

Fluent UI の公式リポジトリでも React 版と Web Components 版は別のパッケージです。ここでは実装を混同しないための入口として触れるにとどめ、具体的なアクセシビリティ評価は次回に扱います。Fluent UI Web Components Fluent UI React Components

次回は「Fluent UI は Shadow DOM とアクセシビリティをどう両立しているのか」を題材に、境界をまたぐフォーカス、Portal、イベント、情報の扱いを見ます。

おわりに

今回の観察から持ち帰れるのは、Shadow Root の mode だけを見てアクセシビリティ情報の公開可否を判断できないことです。ユーザーに必要な意味や操作が届くか、対象ブラウザーと支援技術の組み合わせで確かめる必要があります。

Shadow DOM が隠す実装と、ユーザーに公開すべき意味は別です。大切なのは内部構造が見えるかではなく、コンポーネントが何であり、どう操作でき、今どのような状態なのかがユーザーまで届くことです。前回の記事でたどった情報の経路を、今回は Shadow DOM を含むコンポーネントにも当てはめて考えました。具体的なライブラリの実装は、次回 Fluent UI を題材に確認します。

HTML / UI Framework
        ↓
Light DOM / Shadow DOM
        ↓
ブラウザーが計算するアクセシビリティ情報
        ↓
Platform Accessibility API
        ↓
支援技術
        ↓
ユーザー

この図も理解のための概念モデルであり、実装の全段階やツリー構造を完全に表現するものではありません。HTML-AAM 1.0 Core-AAM 1.2

参考資料

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?