12k
All articles

`inert`属性によるフォーカスとインタラクティビティの管理

inert属性でモーダル、ドロワー、読み込み中のオーバーレイを無効化し、フォーカス、クリック、アクセシビリティツリーへのアクセスを遮断します。

OpenReplay Team
OpenReplay Team
`inert`属性によるフォーカスとインタラクティビティの管理

要素にinertを設定すると、そのサブツリー全体がタブオーダーから除外され、すべてのポインターイベントおよびクリックイベントがブロックされ、スクリーンリーダーによる検出・読み上げができないようアクセシビリティツリーから隠蔽されます。これら3つの動作はWHATWG HTML Living Standardによって仕様として規定されています。さらに、現行ブラウザではページ内検索(Ctrl/Cmd+F)でサブツリー内のテキストがヒットしなくなり、テキスト選択も無効化されます。これらの動作は仕様上はユーザーエージェントの裁量に委ねられていますが、MDNでは実装済みの標準動作として記載されています。つまり、たった1つの属性で6つのインタラクションチャネルを無効化できるということです。

本記事では、モーダル・ドロワー・サイドナビが開いた際にバックグラウンドのコンテンツをクリーンに無効化する方法という具体的な問題を解決します。JavaScriptでフォーカストラップを自作してモバイルスクリーンリーダーを壊したり、DOMにaria-hiddenを散在させたりすることなく、これを実現する方法を解説します。inertがブロックする対象、HTMLおよびJavaScriptでの適用構文、フォーカス復元を含む完全なモーダルの実装例、inertコンテンツのスタイリング方法、そしてdisabledaria-hiddenhiddenを使うべき場面についても取り上げます。

重要なポイント

  • inertは1つの宣言で6つのインタラクションチャネルをブロックします。フォーカス、ポインター/クリックイベント、タブオーダー、アクセシビリティツリーからの検出(いずれも仕様で規定)に加え、ページ内検索とテキスト選択(UAの裁量だが現行ブラウザで実装済み)が対象です。
  • inert2023年4月にBaseline Newly availableとなり、Chrome・Edge 102、Firefox 112、Safari 15.5でサポートされ、2025年10月頃にBaseline Widely availableに達しました。これにより、wicg-inertポリフィルは本番環境での必須要件ではなくレガシーな文脈のものとなっています。
  • 考え方の転換は「ガード対トラップ」です。フォーカストラップはJavaScriptでユーザーをコンポーネント内に閉じ込めますが、inertはページの残りの部分をガードすることで、ブラウザがネイティブに境界を強制します。
  • HTMLのboolean属性に加え、inertHTMLElement.inert IDLプロパティとしても公開されており、JavaScriptでboolean値として設定できます。開く際はmainEl.inert = true、閉じる際はmainEl.inert = falseとするだけです。
  • <dialog>.showModal()はページの残りの部分を自動的にinert化するため、手動でのinert管理はネイティブ要素を使わないカスタムダイアログパターンにのみ必要です。

inert属性がブロックするもの

inertグローバルHTML属性であり、要素とそのサブツリー全体を非インタラクティブかつ検出不能にします。WHATWG HTML Living Standardのinertサブツリーのセクションによれば、inertなノードはクリックやフォーカスなどのユーザーインタラクションイベントを受け取らず、ユーザーエージェントはそれをアクセシビリティツリーから除外しなければなりません。以下の3つのブロックは規範的であり、すべての実装で一貫しています。

  1. フォーカス — inert要素はTab、クリック、またはプログラムによるelement.focus()でフォーカスを受け取れません。
  2. ポインターおよびクリックイベント — ユーザーが起こしたクリックやポインターイベントはinertノードに到達しません。
  3. アクセシビリティツリーからの検出 — 支援技術はそのサブツリーを検出・読み上げできません。

さらに2つのブロックは仕様上ユーザーエージェントの裁量(仕様では規範的なmayが使用されています)に委ねられていますが、MDNでは現行ブラウザ全体で実装済みの動作として記載されています。

  1. ページ内検索 — Ctrl/Cmd+Fはinertサブツリーのテキストにマッチしません。
  2. テキスト選択 — ユーザーはinertなテキストを選択できません。

タブオーダーはフォーカスブロックの結果として生じます。inertノードはフォーカスを受け取れないため、順次フォーカスナビゲーションから完全に除外されます。重要な境界として、inertがブロックするのはユーザーが起こしたイベントのみであり、プログラムによるイベントはブロックされません。dispatchEvent()の呼び出しや、inertサブツリー内で発火するタイマーは引き続き実行されます。inertalert()ではなく、JavaScriptの実行を停止するものではありません。

注意すべき落とし穴として、inertはサブツリーをアクセシビリティツリーから削除するため、ユーザーがまだ読む必要のあるコンテンツには絶対に適用しないでください。視覚的に非表示にしつつ検出可能な状態を保ちたい場合は、別のツールを使用してください。

2つの代表的なユースケース

inertweb.devのinertガイドに記載されている2つの状況のために存在します。DOMに存在するが画面外または非表示の場合と、DOMが表示されているがインタラクティブにすべきでない場合です。

画面外または非表示のDOM。 スライドアウト式のナビゲーションドロワーやサイドナビは、表示される前からフォーカス可能なリンクをDOMに追加します。inertがなければ、キーボードユーザーは閉じたドロワーにTabで入り込み、見えないコントロールにフォーカスしてしまいます。ドロワーのコンテナを開くまでinertとしてマークすることで、それらのリンクをタブオーダーから除外できます。

<nav id="drawer" inert>
  <a href="/dashboard">Dashboard</a>
  <a href="/settings">Settings</a>
</nav>

表示されているが非インタラクティブなUI。 フォームの送信中、ページの読み込み中、またはモーダルオーバーレイがバックグラウンドのコンテンツの上に表示されている場合、そのコンテンツは視覚的に存在していますが入力を受け付けるべきではありません。送信中にフォームにinertを適用することで、二重送信や意図しないフォーカス移動を防げます。

<form id="signup" inert>
  <!-- リクエストの処理中はフィールドをグループとして無効化 -->
</form>

どちらのケースも同じロジックを共有しています。コンテンツはDOM内に留まり(レイアウト、トランジション、状態が維持される)、ブラウザはそこへのインタラクションのルーティングを拒否します。

構文:HTML属性とHTMLElement.inertプロパティ

inertには2つのインターフェースがあります。HTMLのboolean属性とHTMLElement.inert IDLプロパティです。属性は静的またはサーバーレンダリングされたマークアップ向けで、プロパティはJavaScriptで状態をトグルする際に使用します。

boolean属性として、その存在自体が意味を持ちます。inertinert=""は同等であり、デフォルト値はfalse(属性がなければインタラクティブ)です。

<main inert>
  <!-- ここにあるすべてのものは非インタラクティブ -->
</main>

実行時にトグルするには、HTMLElement.inertプロパティを使用します。これはboolean値として直接読み書きできるため、setAttribute / removeAttributeの操作は不要です。

const mainEl = document.querySelector('main');

// ページの残りの部分へのインタラクションを無効化
mainEl.inert = true;

// 復元する
mainEl.inert = false;

これはAPIの中で最もシンプルな部分であり、既存の多くの解説記事で触れられていない点でもあります。開閉のトグルはたった2つの代入で完結します。後述する8ステップのフォーカストラップ手順と比較してみてください。

inert以前:フォーカストラップとその脆弱性

inertが登場する以前、モーダルの境界を設ける標準的な方法はJavaScriptのフォーカストラップでした。TabとShift+Tabを横取りして、ダイアログ内でフォーカスをループさせるロジックです。CSS-Tricksが列挙している標準的な手順はおよそ8ステップに及びます。ページ上のすべてのフォーカス可能な要素を見つけ、モーダル内の最初と最後のフォーカス可能な要素を特定し、外部のすべての要素からインタラクティビティと検出可能性を取り除き、フォーカスを移動させ、閉じるイベントをリッスンし、閉じる際にすべてを復元し、トリガーにフォーカスを戻す、という流れです。

最初のステップ「すべてのフォーカス可能な要素を見つける」自体がバグの温床です。なぜなら、ネイティブにフォーカス可能な要素のセットは多くの人が思っているより大きいからです。tabindexなしで順次タブオーダーにフォーカスを受け取る要素は以下の通りです。

  • hrefを持つ<a><area>
  • <button><input><select><textarea>disabledでない場合)
  • <iframe><embed><object>
  • controls属性を持つ<audio><video>
  • <summary><details>内の最初のもの)
  • 非負のtabindexを持つ任意の要素、およびcontenteditable要素

トラップを構築する際に1つでも見落とすとキーボードユーザーが脱出してしまい、リストを過剰に管理するとブラウザ自身の順序付けと衝突します。より安全なデフォルトは、ドキュメントの自然なフォーカスオーダーをそのままにして、コンポーネントが本当に必要とする場合にのみ介入することです。これがinertが手動作業の大部分を取り除く理由です。

考え方の転換こそが重要です。フォーカストラップはキープレスを横取りすることでユーザーをコンポーネント内に閉じ込めますが、inertはダイアログの外側にあるすべてのものを到達不能にすることでページの残りの部分をガードします。境界を強制するのはJavaScriptではなくブラウザです。このガード対トラップという考え方はLogRocketによるこの属性の解説に由来しています。

手作りのトラップは以下の3つのパターンで失敗します。

  • モバイルの支援技術。 AndroidのTalkBackとiOSのVoiceOverはTabキーではなくスワイプジェスチャーでナビゲートします。キーボードイベントのみを横取りするJavaScriptのトラップは、スワイプベースのスクリーンリーダーユーザーに対してはまったく境界を提供しません。inertはプラットフォームレベルでサブツリーをブロックするため、キーボードとジェスチャーナビゲーションの両方をカバーします。
  • aria-hiddenの散在。 inert以前の回避策は、モーダル以外のすべての要素にaria-hidden="true"を設定することでした。DOMツリーが深いページではこれは管理不能となり、しばしば不完全になります。
  • 手動のタブループ。 Tab/Shift+Tabの横取りロジックは脆弱で、特にモーダルのフォーカス可能なコンテンツが変化する場合に誤りやすいです。

モーダルやドロワーの実装のセッションリプレイでは、ダイアログが開いている間にバックグラウンドのコンテンツにフォーカスイベントが到達するケースが頻繁に見られます。これはフォーカス境界が不完全であることを示すシグネチャであり、まさにinertが解決するために設計された問題です。

上記の参考資料では完全なtrap-focus.jsを再構築していますが、ここで繰り返す必要はありません。重要な比較はコード行数です。トラップは数十行のイベント横取りコードになります。inertを使った同等のコードは以下の通りです。

function openModal() {
  mainEl.inert = true;
}
function closeModal() {
  mainEl.inert = false;
}

フォーカス復元を含むモーダルの実装例

最もクリーンなカスタムモーダルのパターンは、ダイアログを<main inert>の兄弟要素として配置することです。モーダルはinertサブツリーの外側に位置するため、<main>内のすべてがシールされた状態でもインタラクティブを保ちます。この<main inert>兄弟パターンはCSS-Tricksが記載している構造に従っています。以下の例では、多くの参考資料が省略している部分、すなわち開く際にダイアログにフォーカスを移動し、閉じる際にトリガーにフォーカスを復元する処理を追加しています。

<button id="open-modal" type="button">Save changes…</button>

<div
  id="modal"
  class="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <h2 id="modal-title">Save changes?</h2>
  <p>Your unsaved changes will be lost.</p>
  <button id="save" type="button" autofocus>Save</button>
  <button id="cancel" type="button">Discard</button>
</div>

<main id="page">
  <!-- すべてのページコンテンツ -->
</main>
const triggerEl = document.getElementById('open-modal');
const modalEl = document.getElementById('modal');
const mainEl = document.getElementById('page');
const cancelEl = document.getElementById('cancel');

let lastFocused = null;

function openModal() {
  lastFocused = document.activeElement;   // トリガーを記憶する
  modalEl.hidden = false;
  mainEl.inert = true;                    // ページの残りの部分をガードする
  // ダイアログのプライマリアクションにフォーカスを移動する
  modalEl.querySelector('[autofocus]').focus();
}

function closeModal() {
  mainEl.inert = false;                   // ページを復元する
  modalEl.hidden = true;
  if (lastFocused) lastFocused.focus();   // トリガーにフォーカスを復元する
}

triggerEl.addEventListener('click', openModal);
cancelEl.addEventListener('click', closeModal);
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !modalEl.hidden) closeModal();
});

正確性に関するいくつかの注意点を挙げます。ダイアログはフォーカス可能な子要素を通じて暗黙的にtabindex="-1"のセマンティクスを持ちます。一般的に、どこにも正のtabindexは必要ありません。正の整数は自然なタブオーダーを上書きするアンチパターンとして記載されていますtabindex="-1"はプログラムで非インタラクティブなコンテナにフォーカスする必要がある場合にのみ使用し、tabindex="0"は本当にインタラクティブなカスタム要素にのみ使用してください。プライマリアクションのautofocus属性は、ダイアログ内のフォーカスの開始点として仕様が推奨する方法です。このパターンはChrome 102以降、Firefox 112以降、Safari 15.5以降で動作します。

inertコンテンツのスタイリング:デフォルトスタイルは存在しない

inertにはデフォルトの視覚的効果がありません。ブラウザは見た目ではなく動作を変更するため、スタイルを適用しない限りinertコンテンツはアクティブなコンテンツと見た目が同じです。web.devに示されている標準パターンは、[inert]属性セレクタをターゲットとし、属性がブロックするインタラクションチャネルを反映した3つのプロパティを組み合わせます。

[inert],
[inert] * {
  opacity: 0.5;         /* 視覚的な減光 — 「非アクティブ」を示す */
  pointer-events: none; /* ホバー/カーソルのアフォーダンスを抑制する */
  user-select: none;    /* テキスト選択を防ぐ */
  cursor: default;
}

各プロパティには意味があります。opacityは無効状態を視覚的に伝え、pointer-events: noneはインタラクティビティを示唆するホバー状態やカーソル変化を除去し、user-select: noneは属性がすでに適用しているテキスト選択ブロックと一致します。動作はinert自体によって強制されます。CSSは、ブラウザが内部で強制している境界を視覚的なユーザーが確認できるようにするために存在します。

inertdisabledaria-hiddenhiddenpointer-eventsの使い分け

スコープとアクセシビリティツリーの動作によってツールを選択してください。inertはサブツリー全体にわたってすべてのインタラクションチャネルをブロックし、アクセシビリティツリーから削除します。disabledは単一のコントロールをブロックしますが検出可能な状態を保ちます。aria-hiddenはクリックとフォーカスをそのままにしつつ支援技術からコンテンツを隠します。hiddenはコンテンツを完全に削除します。CSSのpointer-events: noneはマウスのみをブロックします。モーダル、ドロワー、またはローディングオーバーレイの背後にあるバックグラウンドコンテンツをシールする必要がある場合はinertを使用してください。

ツールインタラクションをブロックアクセシビリティツリーに存在するかスコープ使用する場面
inertはい(フォーカス、ポインター、検索、選択)いいえ — 削除される要素+サブツリーモーダル、ドロワー、ローディングオーバーレイの背後のバックグラウンドコンテンツをシールする場合
disabledはい(コントロールに対して)はい — 利用不可として読み上げられるフォームコントロールまたはフィールドセットグループ一時的に操作できない単一のボタン、入力、またはフォームセクション
aria-hidden="true"いいえ — クリック/フォーカスは引き続き機能するいいえ — 削除される要素+サブツリー装飾的または重複したコンテンツを支援技術からのみ隠す場合
hidden / display:noneはい — 完全に削除されるいいえ — レンダリングされない要素+サブツリー現時点で視覚的にも支援技術に対しても存在すべきでないコンテンツ
pointer-events: noneマウスのみ — キーボード/支援技術は影響を受けないはい要素+サブツリー装飾的なクリックスルー。inertの代替としては使用不可

よくある2つの誤りとして、バックグラウンドコンテンツにaria-hiddenを使用しつつクリックとフォーカスを残すケース(タブオーダーに残ったまま)と、pointer-events: noneを使用してキーボードとスクリーンリーダーユーザーもブロックされると思い込むケースがあります。バックグラウンドを完全にシールするには、すべてのチャネルをカバーできる唯一のツールとしてinertを使用してください。

dialog.showModal()を使う場合もinertは必要か?

HTMLDialogElement.showModal()でダイアログを開くと、ブラウザは自動的にページの残りの部分をinert化します。トップレイヤーの動作には暗黙的なinert境界が含まれており、属性を手動で管理しなくてもダイアログの外側にあるすべてのものがクリックもタブ移動もできなくなります。手動でのinert管理は、上記の実装例のようにネイティブの<dialog>要素を使わないカスタムダイアログパターンを構築する場合にのみ必要です。

<dialog id="confirm">
  <p>Delete this item?</p>
  <button>Delete</button>
  <button>Cancel</button>
</dialog>
document.getElementById('confirm').showModal(); // ページが自動的にinert化される

showModal()と共に<dialog>を使用できる場合は、inert境界が自動的に提供されます。支援技術のサポートに関する懸念やデザイン上の制約からカスタムダイアログが必要な場合に、手動でのinertを使用してください。

ブラウザサポートと新しいCSS interactivityプロパティ

inertは2023年4月にBaseline Newly availableとなりました。Chrome・Edge 102、Firefox 112、Safari 15.5でサポートされ、2025年10月頃にBaseline Widely availableに達しました(相互運用可能な日付から30ヶ月後)。wicg-inertポリフィルは現在、本番環境での必須要件ではなくレガシーな文脈のものとなっています。最終リリースはv3.1.3(2023年)であり、現在はアクティブにメンテナンスされていません。そのREADMEには、ポリフィルが「かなりの量のツリーウォーキングを必要とする」ため「パフォーマンス面でコストが高い」と記載されており、ネイティブ実装ではこのコストが回避されています。2023年以降にリリースされたブラウザであれば、ポリフィルは不要です。

新しいCSSベースの代替としてinteractivityプロパティがあります。interactivity: inertを指定することで、属性ではなくスタイルシートからinertの動作を適用できます。これは新興の機能であり、サポート範囲は限定的です。caniuseのデータによると、2026年半ば時点でChromiumのみ(Chrome/Edge 135以降、2025年3月)のサポートであり、FirefoxとSafariはサポートしておらず、Baseline(Limited availability)にも達していません。Chromiumのみの環境向けの将来的な選択肢として位置づけ、属性のクロスブラウザ代替としては扱わないでください。

まとめ

モーダル、ドロワー、またはローディングオーバーレイの背後にあるバックグラウンドコンテンツをシールするには、inertが手作りのフォーカストラップやaria-hiddenの散在という脆弱な仕組み全体を置き換えます。キーボード、ポインター、支援技術によるナビゲーションのすべてにわたってブラウザが強制する1つの属性で実現できます。既存のダイアログを見直してください。キーボードユーザーが開いているモーダルからページの背後にTabで出られる場合は、そのバックグラウンドコンテンツまたは<main>inertでラップし、開閉時にelement.inertでトグルし、トリガーにフォーカスを復元してください。この属性は2025年以降Widely availableとなっており、残る判断はネイティブの<dialog>が境界を自動的に提供するかどうかだけです。

よくある質問

`inert`属性と`aria-hidden`の違いは何ですか?

`inert`属性はインタラクションをブロックし、コンテンツをアクセシビリティツリーから削除するため、サブツリーは到達不能かつ検出不能になります。`aria-hidden`属性はコンテンツをアクセシビリティツリーからのみ削除し、クリック、フォーカス、キーボードインタラクションはブロックしません。バックグラウンドコンテンツに`aria-hidden`を適用しつつクリックとフォーカスを残すのはよくある誤りであり、それらの要素はタブオーダーに残ったままになります。サブツリー全体のインタラクションをブロックする必要がある場合は`inert`を使用してください。

`inert`はJavaScriptのイベントリスナーやプログラムによるイベントをブロックしますか?

いいえ。`inert`属性はクリック、フォーカス、ポインターインタラクションなどのユーザーが起こしたイベントをブロックしますが、プログラムによるイベントは停止しません。`dispatchEvent`の呼び出し、タイマーコールバック、またはinertサブツリー内で実行されるスクリプトは通常通り実行されます。`alert`関数とは異なり、`inert`はJavaScriptの実行を停止しません。マークされたサブツリーへのユーザーインタラクションのルーティングとアクセシビリティの検出方法を変えるだけです。

`inert`のJavaScriptポリフィルはまだ必要ですか?

2023年以降にリリースされたブラウザであれば不要です。`inert`属性は2023年4月にBaseline Newly availableとなり、Chrome・Edge 102、Firefox 112、Safari 15.5でサポートされ、2025年10月頃にBaseline Widely availableに達しました。wicg-inertポリフィルは現在、本番環境での必須要件ではなくレガシーな文脈のものとなっています。最終リリースは2023年のv3.1.3であり、現在はアクティブにメンテナンスされていません。そのREADMEには、ネイティブ実装が回避しているツリーウォーキングを必要とするためパフォーマンス面でコストが高いとも記載されています。

従来のフォーカストラップがモバイルスクリーンリーダーで失敗するのはなぜですか?

従来のフォーカストラップがモバイルで失敗するのは、AndroidのTalkBackとiOSのVoiceOverがTabキーではなくスワイプジェスチャーでナビゲートするためです。キーボードイベントのみを横取りするJavaScriptのトラップは、スワイプベースのスクリーンリーダーユーザーに対してはまったく境界を提供せず、ダイアログからバックグラウンドコンテンツに脱出できてしまいます。`inert`属性はプラットフォームレベルでサブツリーをブロックするため、キーボードナビゲーションとジェスチャーナビゲーションの両方をカバーします。これが手作りのフォーカストラップロジックの代わりに`inert`でモーダルの境界を設ける理由です。

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.