0
0

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の向こう側へ、UIの「意味」はどう届く?

0
Posted at
<fluent-button>送信</fluent-button>

スクリーンリーダーは、なぜこれを「送信、ボタン」と認識できるのでしょうか?

fluent-button というタグ名を見て、ブラウザーが自動的に「これはボタンだ」と判断しているわけではありません。では「button」という意味はどこで生まれて、どう支援技術まで届くのでしょうか?

その道筋を、Web Platform の仕組みと Fluent UI Web Components の実装をたどりながら見ていきます。中心にあるのは、実装は隠す。でも、意味は隠さない。 という考え方です。

1. 見た目がボタンであることと、ボタンとして伝わることは違う

まず、次の 2 つを比べてみます。

<button>送信</button>
<fluent-button>送信</fluent-button>

どちらも画面上では、同じような形にスタイルできます。しかし、HTML の <button> はブラウザーが意味を知っているネイティブ要素です。HTML Accessibility API Mappings(HTML-AAM)は、HTML 要素と属性をプラットフォームのアクセシビリティ API へどう対応付けるかを定めており、ネイティブ要素の意味もその対象です。

一方、<fluent-button> は Custom Elements API で定義される独自要素です。名前に button が含まれていても、その名前だけでネイティブの <button> と同じ意味や振る舞いが与えられるわけではありません。独自要素の定義は、タグを見たブラウザーにその機能を教えるためのものです。タグ名だけでは、役割やキーボード操作までは決まりません。

HTML-AAM が示すように、意味を持つ HTML 要素はその意味をアクセシビリティ API に公開できます。見た目がボタンであることと、ブラウザーがボタンとして扱えることは別の話です。

参照:HTML Accessibility API Mappings、WHATWG HTML — Custom elements

2. Shadow DOMは何を隠すのか

Web Components は、Custom Elements、Shadow DOM、HTML <template> などを組み合わせて、再利用できるコンポーネントを作るための Web Platform の仕組みです。

Shadow DOM は、ホスト要素に別の DOM ツリーを関連付ける仕組みです。内部の DOM やスタイルをコンポーネントの外側から扱いにくくし、コンポーネント同士の実装の衝突を避けやすくします。たとえば、コンポーネント内部のクラス名やレイアウト構造を利用側が直接知る必要はありません。

ただし、ここでいう「隠す」は、主に DOM やスタイルの実装境界を作ることです。ブラウザーが内部要素をどのようにアクセシビリティへ表現するかを、Shadow DOM の仕様だけで一律に「隠す」と決めているわけではありません。

では、内部実装を隠したら、アクセシビリティ上の意味まで隠れてしまうのでしょうか?

参照:MDN — Using shadow DOM、WHATWG DOM — Shadow trees

3. DOM TreeとAccessibility Treeは同じものではない

ここでは、コンポーネントの JavaScript、ブラウザーのアクセシビリティ処理、支援技術を別のレイヤーとして考えます。

  • コンポーネントの JavaScript は、属性やプロパティを内部の native control に反映したり、ElementInternals にデフォルトの意味を設定したりします。
  • ブラウザーは DOM / Shadow DOM、slot の割り当て、HTML / ARIA の意味、ElementInternals の値を解釈し、公開する role・name・state などを決めて Accessibility Tree を構成します。
  • さらにブラウザーが、その情報を OS ごとの Platform Accessibility API に対応付けます。スクリーンリーダーなどの支援技術は、その API を介して情報を取得し、ユーザーに提示します。

つまり JavaScript は意味の材料を設定できますが、Accessibility Tree を作って OS の API へ橋渡しする処理そのものを担当するわけではありません。

流れを簡略化すると、次のようになります。

Component JavaScript
   ├─ v2: host の属性・プロパティ → 内部 native <button>
   └─ v3: host の ElementInternals に role などを設定
                 +
DOM / Shadow DOM / slot の内容
                 ↓
              Browser
   ├─ HTML / ARIA / slot / ElementInternals を解釈
   └─ Accessibility Tree を構成
                 ↓
   Platform Accessibility API へ写像
                 ↓
     Assistive Technology
                 ↓
               User

Accessibility Tree(アクセシビリティツリー)は、UI を支援技術が理解しやすい形で表した構造です。要素ごとに、たとえば次のような情報が含まれます。

  • role: 何の UI なのか。たとえば button、link、textbox
  • name: その UI を何と呼ぶか。たとえばボタンの「送信」
  • state: 現在どのような状態か。たとえば disabled、checked、expanded
  • description: name を補足する説明

WAI-ARIA 1.2 は DOM Tree と Accessibility Tree を並行する構造として説明し、支援技術に公開すべき要素について accessible object が作られるとしています。HTML-AAM と Core-AAM は、HTML や ARIA の意味をプラットフォームのアクセシビリティ API へどう対応付けるかを扱います。accessible name や description の計算方法は、Accessible Name and Description Computation(AccName)が定めます。

ここで大切なのは、DOM の各ノードがそのまま一つずつ Accessibility Tree のノードになるわけではないことです。ブラウザーは意味のない要素を省いたり、複数のテキストノードを一つの名前にまとめたり、ARIA や表示状態を加味した表現を作ったりします。Chromium の説明でも、アクセシビリティツリーは HTML の構造に似ていても簡略化され、プラットフォームごとに異なる API へ対応付けられるとされています。

Windows では UI Automation など、macOS では Accessibility API、Linux では ATK / AT-SPI などが使われます。スクリーンリーダーはページの HTML や DOM を直接読み上げるのではなく、ブラウザーが公開するプラットフォーム API を通して情報や操作を受け取ります。

参照:WAI-ARIA 1.2 — Accessibility Tree、Core-AAM 1.2、HTML-AAM 1.0、AccName 1.2、Chromium — Accessibility Overview、MDN — Accessibility tree

なお、ここで参照した Core-AAM 1.2、HTML-AAM 1.0、AccName 1.2 は、2026 年 10 月に確認した時点では W3C 勧告ではなく、それぞれ草案です。仕様の策定状況と、ブラウザーで実際にどう実装されているかは分けて考えます。

4. Shadow DOMとAccessibility Treeの関係

DOM Tree と Accessibility Tree が同じでないのなら、Shadow DOM の内部はどのように表現されるのでしょうか?

DOM Standard では、Shadow DOM はホスト要素に付く別のツリーとして定義されています。また <slot> は、Shadow Tree の中でホスト側の子要素を配置する仕組みです。つまり、コンポーネント内部の Shadow Tree と、利用側が記述した Light DOM は別の DOM 構造ですが、slot を通して一緒に描画できます。

この DOM 上の境界が、そのままアクセシビリティ上の境界になるとは限りません。ブラウザーは DOM を手掛かりにしつつ、表示される内容やセマンティクスをアクセシビリティ表現へ対応付けます。仕様が定めるのは DOM のツリーや slot の振る舞い、HTML / ARIA から各 API への対応付けです。あらゆるブラウザーで同一の Accessibility Tree を作る、という 1 対 1 の変換規則ではありません。

たとえば <fluent-button>送信</fluent-button> では、「送信」は利用側が Light DOM に置いたテキストです。コンポーネントの template は Shadow DOM 内の <slot> を挿入位置として用意し、slot の割り当てによってそのテキストが表示されます。コンポーネントの JavaScript が必ず「送信」を読み取って aria-label にコピーしている、という意味ではありません。ブラウザーが role や表示される内容をもとに accessible name を計算します。

Shadow Tree 内に native <button> がある v2 型の構造でも、役割は分かれています。JavaScript は必要な属性・プロパティをそのボタンへ反映し、ブラウザーは native button の意味と内容からアクセシビリティ表現を作ります。AccName 1.2 の草案には accessible name / description の計算時に、ホストの Shadow Root の子ノードや slot の assigned nodes をたどる手順が追加されています。ただし、これは名前や説明を計算するアルゴリズムについての記述であり、Accessibility Tree 全体の構造が全ブラウザーで同じになるという意味ではありません。

実際、W3C ARIA Working Group の issue #2173 では、slot を含む Shadow DOM 内のリスト構造について、Chrome と Firefox で listitem の role の扱いが異なる例が報告されました。ただし、コメントではこのケースは Shadow DOM 固有ではなく、Firefox が途中にある generic な要素を越えてリスト項目を判定するかどうかの問題かもしれない、と指摘されています。報告者もその特定の例については同意しつつ、Accessibility Tree を DOM Tree と flat tree のどちらに基づけるかという、より広い問いは残ると述べています。

この issue は現在クローズされているため、継続中のブラウザー不具合を示す根拠としてではなく、slot / Shadow DOM と role の親子関係を単純化できない事例として参照します。Shadow DOM は実装境界を作りますが、「内部の意味を支援技術に伝えてはいけない」という境界ではありません。一方で、個々のノードがどう公開されるか、名前がどう計算されるかは、対象のブラウザーやアクセシビリティ API も確認する必要があります。

参照:WHATWG DOM — Shadow trees / Slots、HTML-AAM 1.0、AccName 1.2 — rendered child nodes、W3C ARIA Working Group issue #2173 と コメント

5. Fluent UI Web Components v2ではどうしていたのか

ここからは、実際のコンポーネント実装を見ていきます。バージョンを混ぜないよう、v2 は @fluentui/web-components_v2.6.1 タグのソースを確認しました。このタグが指す Fluent UI の commit は bd8b04ae4863bae7bd22988ae247b9397f405eba です。依存する FAST Foundation 2.49.6 と FAST Element 1.13.0 のソースも、Fluent UI v2.6.1 の yarn.lock が解決するバージョンを含む FAST の commit b8ef4b40bb7be05414dd9c18da137c5b31b3707f に固定しています。

Fluent UI のその commit にある Button 実装は、@microsoft/fast-foundation から Button と buttonTemplate を import し、Foundation Button を継承して Fluent 固有の appearance などを加えています。パッケージの package.json にも、@microsoft/fast-foundation の依存が記載されています。

FAST Foundation の Button は、FormAssociatedButton を継承します。そして Button クラスには control: HTMLButtonElement というプロパティがあります。

これは Custom Element のホストを指すのではありません。buttonTemplate が Shadow DOM 内に作る <button class="control"> に ref("control") を設定し、内部のネイティブボタンを control として参照しています。したがって、ここでの control は「コンポーネント内部の操作対象である HTMLButtonElement」です。ソース上の関係を抜き出すと、次のとおりです。

// Button クラス
public control: HTMLButtonElement;
html`
  <button class="control" part="control">
    ...
    ${ref("control")}
  </button>
`

このテンプレートは、内部の <button> に disabled や form 関連属性を設定し、aria-label、aria-labelledby、aria-disabled、aria-expanded、aria-pressed などの ARIA 属性も個別にバインドしています。ホスト側のプロパティが変わると FAST のテンプレートバインディングが内部ボタンの属性へ反映されます。どの属性を反映するかはテンプレートに明記されており、任意の ARIA 属性をすべて自動で同期する仕組みではありません。

DelegatesARIAButton は aria-expanded と aria-pressed を定義し、さらに ARIAGlobalStatesAndProperties を mixin します。テンプレートはそれらのプロパティの一部を内部ボタンの属性へ結び付けています。ただし、これは すべての ARIA 属性を一律に同期する という意味ではありません。何が反映されるかは、クラスとテンプレートに実際に書かれたプロパティ、属性の範囲で確認できます。

フォーム連携は、この内部ボタンへの属性バインディングとは別の経路です。FAST Foundation の FormAssociated mixin は ElementInternals と attachInternals() を利用できるか feature detection し、利用できる場合はフォーム値を ElementInternals.setFormValue() などで設定します。利用できない場合に使う FormAssociatedButton の proxy は document.createElement("input") で作るフォーム連携用の要素です。control: HTMLButtonElement とは別物なので、フォーム処理用 proxy と Shadow DOM 内のネイティブボタンを混同しないようにします。

このソースから確認できる v2 の流れは、概念的には次のとおりです。

Fluent Custom Element
        ↓
FAST Foundation Button
        ↓
Shadow DOM の buttonTemplate
        ↓
内部の native <button>
        ↓
ブラウザーが native button の意味をアクセシビリティ API へ対応付ける

JavaScript はこの層で、ホストの属性やプロパティを Shadow DOM 内の native button に反映します。これは値を内部コントロールへ設定する処理であり、内部から外へ Accessibility Tree をコピーする処理ではありません。その後、ブラウザーが内部ボタンの native semantics と属性値を解釈してアクセシビリティ表現へ対応付けます。

参照:Fluent UI Web Components v2.6.1 の Button、Fluent UI v2.6.1 の package.json、FAST Foundation 2.49.6 の Button

6. Fluent UI Web Components v3では何が変わったのか

次は、執筆時点の microsoft/fluentui master を 2026 年 10 月 18 日に確認し、commit f02cfa8f8d73ca5652ef2465ce22bf3d06b9dedc に固定して見ていきます。この commit の package.json は @fluentui/web-components 3.1.3 で、@microsoft/fast-element を peer dependency として宣言しています。Button の import にも @microsoft/fast-element が使われています。

つまり、「v3 で FAST を使わなくなった」ということではありません。v2 では FAST Foundation の Button とテンプレートを利用していましたが、v3 では Fluent UI 自身の BaseButton と Button 実装が中心になっています。一方、FAST Element は引き続き使用しています。

ElementInternalsは何をするオブジェクトか

this.attachInternals() は、Shadow DOM の中の要素を取得するメソッドではありません。Custom Element のホスト自身に関連付けられた ElementInternals を取得します。コンポーネントの JavaScript はこのオブジェクトを通じて、ホストのブラウザー向けの情報を設定できます。

アクセシビリティに関係する role や aria* プロパティは、ホストに role="button" などの属性を単純に書き込むのではなく、ホストのデフォルトのセマンティクスを設定します。ページを使う側が明示的に role や aria-* 属性を指定した場合は、その指定がデフォルト値より優先されます。ブラウザーはこの情報を含めてホストの意味を解釈しますが、ElementInternals 自体が Accessibility Tree を作ったり、支援技術を直接呼び出したりするわけではありません。

また、ElementInternals はアクセシビリティ専用ではありません。フォーム連携にも使われ、static formAssociated = true と組み合わせて form、setFormValue()、setValidity() などを扱えます。したがって、attachInternals() を見つけただけで「role を設定している」とは限りません。何のプロパティやメソッドを呼んでいるかを読む必要があります。

参照:WHATWG HTML — Accessibility semantics for custom elements、WHATWG HTML — Form-associated custom elements、MDN — ElementInternals

現行の button.base.ts は FASTElement を継承します。クラスの初期化で this.attachInternals() を呼び、constructor で this.elementInternals.role = 'button' を設定します。また static readonly formAssociated = true とフォーム関連の ElementInternals API を使っています。

ariaDisabled については、disabledFocusable の変更時と connectedCallback で this.elementInternals.ariaDisabled に設定しています。一方、disabled の変更では tabindex を調整しています。コードから読み取れるのはこの実装です。「disabled のすべての状態を、すべての ARIA 属性へ同期する」といった一般化はできません。

なお、v2 の FAST Foundation も ElementInternals を使う場合がありますが、ここで確認した用途はフォーム連携です。v2 の Button の role を成立させる中心は Shadow DOM 内の native <button> であり、v3 では ElementInternals.role をホストに設定している点が異なります。

さらに、button.template.ts のテンプレートに通常の <button> はありません。開始・終了 slot と、コンテンツを入れる <span>、既定の <slot> が中心です。つまり v3 の通常表示では、Shadow DOM の内部 native button に意味を預けるのではなく、ホストの ElementInternals に button role などを設定する構成です。

例外として、form の submit に関する fallback 処理では、コードが動的に <button> を作成する経路があります。ただしこれは通常の Button テンプレートとは別のフォーム処理用 fallback です。ソースを読むときは、この経路とコンポーネントの通常のアクセシビリティ表現を分けて考えます。

この実装で ElementInternals が担っている意味の設定は、WHATWG HTML が示す「Custom Element の default accessible roles, states, and properties」と同じ考え方です。ホストの role をデフォルトの意味として設定し、ブラウザーがそれをアクセシビリティ表現へ対応付けます。

実装の要点を短く抜き出すと、BaseButton はホストに ElementInternals を結び付け、Button の role を設定します。

public elementInternals: ElementInternals = this.attachInternals();
static readonly formAssociated = true;

constructor() {
  super();
  this.elementInternals.role = 'button';
}

通常の template は内部の native <button> を持たず、slot と表示用要素を配置します。

export function buttonTemplate<T extends BaseButton>(options: ButtonOptions = {}): ElementViewTemplate<T> {
  return html<T>`
    <template
      @click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
      @keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
    >
      ${startSlotTemplate(options)}
      <span class="content" part="content">
        <slot ${slotted('defaultSlottedContent')}></slot>
      </span>
      ${endSlotTemplate(options)}
    </template>
  `;
}

disabledFocusable に応じて設定される state も、ホストの ARIA 属性への反映ではなく ElementInternals のプロパティです。

this.elementInternals.ariaDisabled = `${!!this.disabledFocusable}`;

参照:Fluent UI Web Components v3.1.3 の現在の package.json と Button source、WHATWG HTML — Accessibility semantics for custom elements、MDN — ElementInternals

7. v2とv3を比較する

両方の実装は、アクセシビリティの意味を作る場所が異なります。どちらが新しいから優れている、という比較ではありません。ソースで確認できた実装上の違いを整理します。

観点 v2.6.1 のソース v3.1.3 の source commit
🧱 コンポーネントの土台 FAST Foundation Button を継承 Fluent UI 自身の BaseButton が FASTElement を継承
🎭 通常の表示テンプレート Shadow DOM 内に native <button> slot と <span> が中心で、native button はない
♿ Button の意味 内部 native <button> の意味を利用 ElementInternals.role = 'button' をホストに設定
🔁 属性の扱い テンプレートで列挙された属性を内部ボタンへバインド ElementInternals の role / ariaDisabled などを設定
⚙️ FAST の利用 @microsoft/fast-element と @microsoft/fast-foundation @microsoft/fast-element を継続利用。Foundation Button への依存はこの実装にない

図にすると、今回確認した範囲では次のように整理できます。

v2:
Component JavaScript
  host の属性・プロパティを内部 button へバインド
                    ↓
Shadow DOM 内の native <button> + slot の内容
                    ↓
Browser: native semantics と accessible name を計算
                    ↓
Accessibility Tree → Platform Accessibility API

v3:
Component JavaScript
  ElementInternals に role / ariaDisabled を設定
                    +
Shadow DOM の template + slot の内容
                    ↓
Browser: host の意味と内容を解釈
                    ↓
Accessibility Tree → Platform Accessibility API

違いは JavaScript を使うかどうかではなく、JavaScript がどこに何を設定するかです。v2 はホストの属性やプロパティを内部の native <button> に結び付け、v3 はホストの ElementInternals に role などのデフォルト意味を設定します。どちらも JavaScript が UI の意味を成立させる手掛かりを用意しますが、Accessibility Tree の構成と Platform Accessibility API への写像を担うのはブラウザーです。支援技術はその API を利用します。このように、JavaScript の設定、ブラウザーの解釈・写像、支援技術による利用を分けて見ると、情報が届く仕組みを正確に捉えられます。

8. UIライブラリを使えばアクセシビリティは解決するのか

UI ライブラリは、role や state、キーボード操作、フォーカス、フォーム連携などを実装・支援できます。今回見た Button でも、Fluent UI 側が focus や keyboard interaction、form API などの処理を持っています。

しかし、アプリケーションで表示する内容の意味まで、ライブラリがすべて決められるわけではありません。たとえば、検索アイコンだけを表示する Button なら、アイコンから「検索」という accessible name を読み取れるとは限りません。

<fluent-button aria-label="検索" icon-only>
  <svg aria-hidden="true">
    <!-- search icon -->
  </svg>
</fluent-button>

ここでは、アイコンを装飾として扱い、ボタンの名前を aria-label="検索" で明示しています。検索なのか、削除なのか、メニューを開くのか。その画面でボタンが何をするかはアプリケーションの文脈がないと決められません。

責務 例
🧩 ライブラリ role / state の実装、キーボード操作、フォーカス管理、フォーム連携
🏗️ アプリケーション 操作の目的、適切な accessible name、状態の意味、アイコンを装飾として扱うかどうか

ライブラリは意味を伝える土台を作れます。しかし、その UI が何を意味するか を決めるのは、最終的には利用するアプリケーションです。

9. 実装は隠す。でも、意味は隠さない

冒頭のコードに戻ります。

<fluent-button>送信</fluent-button>

fluent-button というタグ名が「ボタン」を意味するのではありません。コンポーネントの実装が、native <button> や ElementInternals などを使って意味を定義し、ブラウザーがそれをアクセシビリティ表現とプラットフォーム API へ対応付けます。支援技術はその情報を受け取り、ユーザーへ伝えます。

Custom Element
       ↓
Web Component
       ↓
Shadow DOM / ElementInternals
       ↓
    Browser
       ↓
Accessibility Tree
       ↓
Platform Accessibility API
       ↓
Assistive Technology
       ↓
      User

この流れを理解すると、Shadow DOM が隠すものと、ブラウザーから支援技術へ伝わるものを分けて考えられます。Shadow DOM による内部実装のカプセル化と、UI の意味をユーザーから隠すことは同じではありません。

実装は隠す。でも、意味は隠さない。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?