言語を切り替える
テーマを切り替える

Dialog、Sheet、Popover:オーバーレイコンポーネントのアクセシビリティとフォーカス管理

深夜2時、クライアントからメールが届きました。「サイトのオーバーレイを開いた後、Tabキーを押すとフォーカスが背景ページに移動してしまいます。キーボードユーザーが操作できません。」

正直、このメールを見た時はかなり恥ずかしかったです。なぜなら、このオーバーレイは私が先週書いたものだったからです。

コードを確認すると、問題は明白でした。Dialogを開いた後、フォーカスが背景のボタンに残っていたのです。ユーザーがTabキーを押すと、当然フォーカスは背景ページに移動します。スクリーンリーダーのユーザーにとってはさらに悪い状況でした。フォーカスがオーバーレイ内に移動していなかったため、ARIA属性も設定されておらず、オーバーレイが開いたことに気づくことができませんでした。

この記事では、Dialog、Sheet、Popoverという3つのオーバーレイコンポーネントのアクセシビリティとフォーカス管理について解説します。正直、私が踏んだ落とし穴を避けていただければと思います。


まず理解する:3つのコンポーネントの核心的な違い

正直、多くの人—私も以前は—この3つのコンポーネントの違いをかなり曖昧に考えていました。単なるオーバーレイでしょう?すべて同じようなものです。しかし実際には、その核心的な違いがアクセシビリティの処理方法を決定します。

Dialog(モーダルダイアログ)

Dialogは背景インタラクションを完全にブロックするモーダルオーバーレイです。

例えば、「注文削除」ボタンをクリックすると、確認ダイアログが表示されます。背景ページはオーバーレイマスクで覆われ、背景のどの要素もクリックできなくなります。これがDialogの核心的な特徴です:ユーザーに現在のタスクを処理させることを強制する

使用シーン:

  • 重要な通知(削除確認、操作警告)
  • フォーム入力(ログインボックス、登録フォーム)
  • ユーザーの即時応答が必要な操作

アクセシビリティの要点:aria-modal="true"を設定する必要があり、フォーカストラップを実装する必要があります。

Sheet(サイドドロワー)

Sheetは画面端からスライドするドロワー形式のパネルです。本質的にDialogと同じです。両方ともモーダルオーバーレイであり、背景インタラクションをブロックし、フォーカストラップが必要です。唯一の違いは視覚的な位置です:Sheetは側面からスライドし、Dialogは中央に表示されます。

使用シーン:

  • ナビゲーションメニュー(モバイルサイドバー)
  • 設定パネル(設定、テーマ切り替え)
  • 詳細表示(商品詳細、記事プレビュー)

正直、「Sheet」という言葉は最初馴染みがありませんでした。後に、これはDrawerの別名であることに気づきました。一部のUIライブラリはDrawerと呼び、一部はSheetと呼びます。Radix UIとshadcn/uiはSheetを使用しています。

アクセシビリティの要点:Dialogと同じ—aria-modal="true"、フォーカストラップ、Escで閉じる。

Popover(ポップアップ)

Popoverは背景インタラクションをブロックしない非モーダルオーバーレイです。

これが重要です—Popoverを開いた後、ユーザーはまだ背景要素をクリックできます。フォーカスはPopover内に強制的に制限されません。

例えば、「その他の操作」ボタンをクリックすると、「編集」、「コピー」、「削除」を含む小さなパネルが表示されます。これがPopoverです。背景の他のボタンをクリックでき、Popoverは自動的に閉じます。

使用シーン:

  • ドロップダウンメニュー(アクションメニュー、オプションリスト)
  • ツールチップ(リッチテキストヒント、使用説明)
  • クイックアクション(編集、コピー、削除)

アクセシビリティの要点:aria-modal="false"(または省略)、フォーカスは強制的にトラップされず、外部クリックで閉じる。

1つの表で違いを明確に

正直、この表を書いている時も確認していたのですが、以前は一部の概念が確かに曖昧でした。

特徴DialogSheetPopover
背景をブロック✅ ブロック必須✅ ブロック必須❌ ブロックしない
フォーカストラップ実装必須実装必須オプション(強制しない推奨)
Escで閉じるサポート必須サポート必須サポート推奨
外部クリックで閉じるオプションオプションデフォルト動作
ARIAロールdialogdialogpopover
aria-modal"true""true""false"または省略
視覚位置中央側面からスライドトリガー要素相対位置

一言でまとめると:DialogとSheetはモーダルオーバーレイ、Popoverは非モーダルオーバーレイ。モーダルオーバーレイはフォーカストラップを実装する必要があり、非モーダルオーバーレイは強制しないことができます。


WCAGアクセシビリティ標準の詳細解説

正直、WCAG標準は最初かなり乾燥していました。大量の英語用語、まるで法律文書のように読めます。しかし実際のプロジェクトで問題に遭遇した後、これらの標準が本当に有用であることに気づきました。検査を通過するためではなく、ユーザーが正常に操作できるようにするためです。

必須ARIA属性

オーバーレイコンポーネントには3つの必須ARIA属性があります:

1. role="dialog"

この属性は支援技術(スクリーンリーダー)に伝えます:これはダイアログです。

<div role="dialog">
  <!-- オーバーレイ内容 -->
&lt;/div>

2. aria-labelledby

この属性はオーバーレイのタイトル要素に関連付けます。スクリーンリーダーがオーバーレイを開いた時、最初にタイトルを読み上げます。

&lt;div role="dialog" aria-labelledby="dialog-title">
  &lt;h2 id="dialog-title">削除確認&lt;/h2>
  &lt;p>この操作は取り消せません。&lt;/p>
&lt;/div>

3. aria-modal="true"(モーダルオーバーレイのみ)

この属性はスクリーンリーダーに伝えます:背景内容はアクセスできません。

&lt;div role="dialog" aria-modal="true">
  <!-- モーダルオーバーレイ内容 -->
&lt;/div>

正直、以前はaria-labelledbyをよく忘れていました。後にスクリーンリーダーでテストした時、気づきました。この属性がないと、ユーザーがオーバーレイを開いた後、何も聞こえず、これが何であるかを知ることができません。

キーボードナビゲーション要件

WCAGはオーバーレイのキーボードナビゲーションに明確な要件を持っています:

Tabキー:オーバーレイ内でフォーカスを循環
ユーザーがTabを押した時、フォーカスはオーバーレイ内のインタラクティブ要素間で循環し、背景ページに逃げ出してはいけません。

Shift+Tab:逆方向循環フォーカス
ユーザーがShift+Tabを押した時、フォーカスは逆方向に循環します。

Escキー:オーバーレイを閉じる
ユーザーがEscを押した時、オーバーレイは閉じる必要があります。これは必須です。一部のユーザーはEscでオーバーレイを閉じる習慣があります。サポートされていない場合、彼らは内部に閉じ込められます。

Enter/Space:ボタンをアクティブ化
これらのキーはボタンまたはリンクをアクティブ化します。

正直、以前はTab循環の落とし穴に遭遇しました。オーバーレイを開いた後、フォーカスが内部に制限されていませんでした。ユーザーがTabを押すと、フォーカスが背景ページに移動しました。これが冒頭で述べたクライアントの苦情でした。

フォーカス管理規範

フォーカス管理はオーバーレイアクセシビリティの中で最も見過ごされる部分です。WCAGの要件は単純です:

オーバーレイを開いた時
フォーカスはオーバーレイ内の最初のインタラクティブ要素(通常は閉じるボタンまたは最初の入力)に移動する必要があります。

オーバーレイを閉じた時
フォーカスはトリガー要素(オーバーレイを開いたボタン)に復元する必要があります。

正直、以前は閉じた後のフォーカス復元を完全に意識していませんでした。後にキーボードでテストした時、気づきました。オーバーレイを閉じた後、フォーカスがどこに行ったのか不明でした。キーボードユーザーは再び探す必要があります。この体験は本当に悪いです。

特殊ケース
オーバーレイに重要な提示内容(例えば操作説明)がある場合、フォーカスは最初にコンテナ要素に置く必要があり、スクリーンリーダーが提示内容を最初に読み上げ、ユーザーが操作できるようにします。

方法はコンテナにtabindex="0"を追加することです:

&lt;div role="dialog" aria-modal="true" tabindex="0">
  &lt;h2>操作説明&lt;/h2>
  &lt;p>操作する前に以下の内容を注意深く読んでください...&lt;/p>
  &lt;button>確認&lt;/button>
&lt;/div>

このように、オーバーレイを開いた時、フォーカスは最初にコンテナに置かれます。スクリーンリーダーは最初に全内容を読み上げ、ユーザーがTabでボタンに移動できるようにします。


フォーカストラップ実装原理

正直、フォーカストラップは複雑に聞こえますが、原理は実際には単純です。Tabキーをオーバーレイ内で循環させるだけです。

フォーカストラップとは

フォーカストラップの定義:ユーザーのTabナビゲーションを特定領域内で循環させるように制限する

例えば:オーバーレイを開いた後、ユーザーがTabを押すと、フォーカスは「閉じるボタン」から「確認ボタン」に移動します。再度Tabを押すと、フォーカスは「閉じるボタン」に戻ります。これがフォーカストラップです。

必要性:ユーザーが誤って背景内容を操作するのを防ぐ。フォーカスが背景ページに逃げ出すことができる場合、ユーザーは誤って背景のボタンをトリガーし、意外な操作を引き起こす可能性があります。

JavaScript実装アプローチ

フォーカストラップの核心ロジックは単純です:オーバーレイ内のすべてのインタラクティブ要素を見つけ、Tabキーを監視し、最初と最後の要素間で循環します。

function trapFocus(modal) {
  // すべてのインタラクティブ要素を見つける
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );

  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  // キーボードイベントを監視
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      // Shift+Tab:最初の要素で最後にジャンプ
      if (e.shiftKey && document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
      // Tab:最後の要素で最初にジャンプ
      else if (!e.shiftKey && document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }

    // Escでオーバーレイを閉じる
    if (e.key === 'Escape') {
      closeModal();
    }
  });
}

正直、このコードは何回も書いて動作させました。主な落とし穴:

  • focusableElementsセレクターは完全である必要があります。あるタイプを漏らすとフォーカスが逃げ出します
  • e.preventDefault()を呼び出す必要があります。そうしないとブラウザのデフォルト動作がフォーカスを外に移動させます

focus-trapライブラリ紹介

自分でフォーカストラップを書きたくない場合、既存のライブラリを使用できます—focus-trap-react

import FocusTrap from 'focus-trap-react';

&lt;FocusTrap>
  &lt;div className="modal">
    &lt;button>閉じる&lt;/button>
    &lt;button>確認&lt;/button>
  &lt;/div>
&lt;/FocusTrap>

このライブラリはフォーカス循環、Esc閉じる、ネストされたオーバーレイなどを自動的に処理します。

正直、今は基本的にこのライブラリを使用していません。shadcn/uiが内部でフォーカス管理を統合しているためです。Radix UI(shadcn/uiの基礎レイヤー)はすべてのフォーカストラップロジックを自動的に処理し、追加ライブラリは必要ありません。


shadcn/ui実践:Dialog実装

正直、shadcn/uiを使用した後、手動でオーバーレイコンポーネントを書くことはありません。怠けているためではなく、手動で書いたオーバーレイは常にアクセシビリティ問題を持っています。shadcn/uiはRadix UIベースで、すべてのアクセシビリティ詳細を自動的に処理します。

インストールと基本使用

npx shadcn@latest add dialog

インストール後、components/ui/dialog.tsxファイルが自動的に生成されます。

完全なコード例

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function DeleteConfirmDialog() {
  return (
    &lt;Dialog>
      &lt;DialogTrigger asChild>
        &lt;Button variant="outline">注文削除&lt;/Button>
      &lt;/DialogTrigger>
      &lt;DialogContent>
        &lt;DialogHeader>
          &lt;DialogTitle>削除確認&lt;/DialogTitle>
          &lt;DialogDescription>
            この操作は取り消せません。この注文を削除してよろしいですか?
          &lt;/DialogDescription>
        &lt;/DialogHeader>
        &lt;div className="flex justify-end gap-2 mt-4">
          &lt;Button variant="outline">キャンセル&lt;/Button>
          &lt;Button variant="destructive">削除&lt;/Button>
        &lt;/div>
      &lt;/DialogContent>
    &lt;/Dialog>
  )
}

正直、このコードは単純に見えますが、背後でRadix UIは多くの詳細を自動的に処理しています:

  • オーバーレイを開いた時、フォーカスは最初のボタン(「キャンセル」)に移動
  • オーバーレイを閉じた時、フォーカスは「注文削除」ボタンに復元
  • Tabはオーバーレイ内で循環
  • Escはオーバーレイを閉じる
  • aria-labelledbyは自動的にDialogTitleに関連付け
  • aria-describedbyは自動的にDialogDescriptionに関連付け

重要なアクセシビリティ機能

1. 自動フォーカス管理

Radix UIのDialogは、開いた時、フォーカスをオーバーレイ内の最初のインタラクティブ要素に自動的に移動します。閉じた時、フォーカスは自動的にトリガー要素に復元します。

2. 自動ARIA属性関連付け

DialogTitleは自動的にaria-labelledbyに関連付け、DialogDescriptionは自動的にaria-describedbyに関連付け。

<!-- Radix UI生成のHTML -->
&lt;div role="dialog" aria-modal="true" aria-labelledby="radix-:r1:" aria-describedby="radix-:r2:">
  &lt;h2 id="radix-:r1:">削除確認&lt;/h2>
  &lt;p id="radix-:r2:">この操作は取り消せません...&lt;/p>
&lt;/div>

正直、これらの詳細は手動で書くと簡単に見逃されます。shadcn/uiを使用すれば、心配ありません。

3. 自動Escキー閉じる

Escを押すとオーバーレイが自動的に閉じ、フォーカスは自動的にトリガー要素に復元します。

4. オーバーレイマスククリックで閉じる

オーバーレイマスク(オーバーレイ外の灰色背景)をクリックすると閉じます。この動作はDialogContentonInteractOutsideプロパティでブロックできます。

&lt;DialogContent onInteractOutside={(e) => e.preventDefault()}>
  <!-- マスクをクリックしてもオーバーレイは閉じません -->
&lt;/DialogContent>

shadcn/ui実践:Sheet実装

Sheetのアクセシビリティ機能はDialogと完全に同じです。唯一の違いは視覚的な位置—Sheetは側面からスライドします。

インストールと基本使用

npx shadcn@latest add sheet

完全なコード例

import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"

export function NavigationSheet() {
  return (
    &lt;Sheet>
      &lt;SheetTrigger asChild>
        &lt;Button variant="outline">メニューを開く&lt;/Button>
      &lt;/SheetTrigger>
      &lt;SheetContent side="left">
        &lt;SheetHeader>
          &lt;SheetTitle>ナビゲーションメニュー&lt;/SheetTitle>
          &lt;SheetDescription>
            アクセスしたいページを選択してください
          &lt;/SheetDescription>
        &lt;/SheetHeader>
        &lt;nav className="flex flex-col gap-4 mt-4">
          &lt;a href="/" className="hover:underline">ホーム&lt;/a>
          &lt;a href="/about" className="hover:underline">概要&lt;/a>
          &lt;a href="/contact" className="hover:underline">連絡先&lt;/a>
        &lt;/nav>
      &lt;/SheetContent>
    &lt;/Sheet>
  )
}

Dialogとの違い

正直、SheetとDialogのコードはほぼ同じで、コンポーネント名が異なるだけです。主な違い:

1. スライドアニメーション

Sheetはデフォルトで右側からスライドします。sideプロパティで方向を制御できます:

&lt;SheetContent side="left">   <!-- 左側からスライド -->
&lt;SheetContent side="right">  <!-- 右側からスライド(デフォルト) -->
&lt;SheetContent side="top">    <!-- 上部からスライド -->
&lt;SheetContent side="bottom"> <!-- 下部からスライド -->

2. 同じアクセシビリティ機能

Sheetのアクセシビリティ機能はDialogと完全に同じです:

  • role="dialog"
  • aria-modal="true"
  • フォーカストラップ、Esc閉じる、フォーカス復元

正直、Sheetコンポーネントは主にモバイルのナビゲーションメニューに使用しています。側面からのスライドの視覚効果は、モバイルのインタラクション習慣により合っています。


shadcn/ui実践:Popover実装

Popoverは非モーダルオーバーレイです。DialogとSheetの核心的な違いは:背景インタラクションをブロックしない

インストールと基本使用

npx shadcn@latest add popover

完全なコード例

import {
  Popover,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Button } from "@/components/ui/button"

export function ActionPopover() {
  return (
    &lt;Popover>
      &lt;PopoverTrigger asChild>
        &lt;Button variant="outline">その他の操作&lt;/Button>
      &lt;/PopoverTrigger>
      &lt;PopoverContent>
        &lt;PopoverHeader>
          &lt;PopoverTitle>クイックアクション&lt;/PopoverTitle>
          &lt;PopoverDescription>
            以下の操作を選択してください
          &lt;/PopoverDescription>
        &lt;/PopoverHeader>
        &lt;div className="flex flex-col gap-2 mt-2">
          &lt;Button size="sm">編集&lt;/Button>
          &lt;Button size="sm">コピー&lt;/Button>
          &lt;Button size="sm" variant="destructive">削除&lt;/Button>
        &lt;/div>
      &lt;/PopoverContent>
    &lt;/Popover>
  )
}

重要な違い

正直、PopoverのコードはDialogやSheetと似ていますが、背後の動作は完全に異なります:

1. 非モーダル

Popoverを開いた後、ユーザーはまだ背景要素をクリックできます。フォーカスはPopover内に強制的に制限されません。

2. フォーカス非強制

ユーザーがTabを押した時、フォーカスはPopoverから背景要素に逃げ出すことができます。これはDialogと完全に異なります。

3. 外部クリックで閉じる

Popover外の任意の要素をクリックすると、Popoverは自動的に閉じます。これはデフォルト動作です。onInteractOutsideプロパティでブロックできます。

&lt;PopoverContent onInteractOutside={(e) => e.preventDefault()}>
  <!-- 外部をクリックしても閉じません -->
&lt;/PopoverContent>

4. 柔軟な位置決め

Popoverはalignプロパティで水平方向の配置を制御します:

&lt;PopoverContent align="start">  <!-- 左揃え -->
&lt;PopoverContent align="center"> <!-- 中央揃え(デフォルト) -->
&lt;PopoverContent align="end">    <!-- 右揃え -->

正直、Popoverは主にアクションメニューに使用しています。ボタンをクリックしていくつかのクイックアクションオプションを表示します。このシーンは背景インタラクションをブロックする必要がなく、Popoverが完全に適しています。


高度な技巧とよくある落とし穴

正直、オーバーレイコンポーネントの落とし穴に多く遭遇しました。ここで最も一般的なものを紹介します。

フォーカス復元の落とし穴:トリガー要素が削除される

シーン:オーバーレイを開いた後、トリガー要素(ボタン)が削除される。オーバーレイを閉じた時、フォーカスの復元先がない。

解決策:

  1. トリガー要素を削除しない、単に隠す
  2. またはフォーカス復元先の要素を記録する
const [triggerElement, setTriggerElement] = useState&lt;HTMLElement | null>(null);

// オーバーレイを開いた時にトリガー要素を記録
const handleOpen = (e: React.MouseEvent&lt;HTMLButtonElement>) => {
  setTriggerElement(e.currentTarget);
  setOpen(true);
};

// オーバーレイを閉じた時にフォーカスを復元
const handleClose = () => {
  setOpen(false);
  triggerElement?.focus();
};

正直、この落とし穴に遭遇しました。ユーザーがレコードを削除した後、オーバーレイが閉じ、フォーカスがどこに行ったのか不明でした。後に、フォーカスをリストの前のレコードに復元して解決しました。

スクリーンリーダーの落とし穴:オーバーレイ内容が読み上げられない

シーン:オーバーレイを開いた後、スクリーンリーダーがオーバーレイ内容を読み上げない。ユーザーはオーバーレイ内の内容が何であるかを知らない。

原因:

  1. aria-labelledby属性が欠けている
  2. フォーカスがオーバーレイ内に移動していない

解決策:
DialogTitleDialogDescriptionが両方設定されていることを確認。shadcn/uiは自動的にARIA属性を関連付けます。

&lt;DialogContent>
  &lt;DialogHeader>
    &lt;DialogTitle>削除確認&lt;/DialogTitle>  <!-- 必須 -->
    &lt;DialogDescription>この操作は取り消せません&lt;/DialogDescription>  <!-- 必須 -->
  &lt;/DialogHeader>
&lt;/DialogContent>

正直、以前はDialogDescriptionをよく漏らしていました。後にNVDA(スクリーンリーダー)でテストした時、気づきました。説明がないと、ユーザーはオーバーレイのタイトルしか知らず、具体的な内容を知ることができません。

ネストされたオーバーレイの落とし穴:フォーカス管理混乱

シーン:オーバーレイAがオーバーレイBを開く。Bを閉じた後、フォーカスがどこに行ったのか不明。

解決策:
Radix UIのDialogとSheetはネストをサポートします。内側のオーバーレイを閉じた時、フォーカスは内側のオーバーレイのトリガー要素(外側のオーバーレイ内のボタンかもしれません)に復元します。

&lt;Dialog>
  &lt;DialogTrigger>オーバーレイAを開く&lt;/DialogTrigger>
  &lt;DialogContent>
    &lt;DialogTitle>オーバーレイA&lt;/DialogTitle>

    <!-- オーバーレイA内でオーバーレイBを開く -->
    &lt;Dialog>
      &lt;DialogTrigger>オーバーレイBを開く&lt;/DialogTrigger>
      &lt;DialogContent>
        &lt;DialogTitle>オーバーレイB&lt;/DialogTitle>
      &lt;/DialogContent>
    &lt;/Dialog>
  &lt;/DialogContent>
&lt;/Dialog>

正直、ネストされたオーバーレイの状況はできるだけ避けています。本当に必要な場合、Radix UIのネストサポートを使用し、フォーカスを自動的に処理させます。

アニメーション遅延の落とし穴:フォーカスがオーバーレイ内にない

シーン:オーバーレイにアニメーションがある(例えばフェードイン)。アニメーション期間中、フォーカスがオーバーレイ内にない。

原因:
アニメーションが開始した時、オーバーレイがまだ完全に表示されていないため、フォーカス設定が失敗する。

解決策:
Radix UIはこの問題を自動的に処理します。アニメーションが完了した後にフォーカスを設定します。

自分で実装する場合、アニメーションが完了するまで待つ必要があります:

modal.addEventListener('animationend', () => {
  const firstFocusable = modal.querySelector('button, [href], input');
  firstFocusable?.focus();
});

正直、この落とし穴に遭遇しました。手動で書いたオーバーレイ、開いた時、フォーカスがまだ背景のボタンにありました。アニメーションが完了する前にフォーカスを設定しようとしたためです。後にanimationendイベントリスナーを追加して解決しました。


まとめ

多くの話をしましたが、核心は3つのポイントです:

1. 3つのコンポーネントタイプの核心的な違い

DialogとSheetはモーダルオーバーレイです—背景インタラクションをブロックし、フォーカストラップが必須。
Popoverは非モーダルオーバーレイです—背景インタラクションをブロックせず、フォーカスは強制されません。

2. WCAGアクセシビリティの3つの要件

ARIA属性(role="dialog"aria-labelledbyaria-modal="true"
キーボードナビゲーション(Tab循環、Shift+Tab逆方向、Esc閉じる)
フォーカス管理(開いた時にフォーカス、閉じた時に復元)

3. shadcn/uiはすべての詳細を自動的に処理

Radix UIはフォーカストラップ、ARIA属性、キーボードナビゲーションを自動的に処理します。shadcn/uiを使用すれば、基本的にアクセシビリティ問題を心配する必要がありません。

正直、多くのオーバーレイコンポーネントを書いた後、私の原則は単純です:本番環境ではshadcn/uiを優先。手動で書いたオーバーレイコンポーネントは常に無限のアクセシビリティ問題があり、shadcn/uiはRadix UIベースで、これらの詳細をすべて処理しています。

唯一注意が必要なのは:原理を理解すること。Radix UIが背後で何をしているかを知れば、問題が発生した時に迅速に位置特定できます。


参考文献


FAQ

Dialog、Sheet、Popoverコンポーネントの違いは何ですか?
DialogとSheetはモーダルオーバーレイです。開いた時に背景インタラクションをブロックし、フォーカストラップを実装する必要があります。Popoverは非モーダルオーバーレイで、背景をブロックせず、フォーカスは自由に移動できます。核心的な違いは背景インタラクションをブロックするかどうかです。
オーバーレイコンポーネントはどのアクセシビリティ要件を実装する必要がありますか?
WCAGは3つの側面を要求します:ARIA属性(role="dialog"、aria-labelledby、aria-modal="true")、キーボードナビゲーション(Tab循環、Shift+Tab逆方向、Escで閉じる)、フォーカス管理(開いた時にオーバーレイ内にフォーカスを移動、閉じた時にトリガー要素に復元)。
フォーカストラップとは何ですか?モーダルオーバーレイはなぜ実装する必要がありますか?
フォーカストラップはユーザーのTabナビゲーションを特定領域内で循環させるように制限する技術です。モーダルオーバーレイはフォーカストラップを実装する必要があり、ユーザーが誤って背景内容を操作するのを防ぎます。フォーカスが背景ページに逃げ出すことができる場合、ユーザーは誤って背景のボタンをトリガーし、意外な操作を引き起こす可能性があります。
shadcn/uiのDialogコンポーネントはどのアクセシビリティ詳細を自動的に処理しますか?
shadcn/uiはRadix UIベースで、以下の詳細を自動的に処理します:

• オーバーレイを開いた時にフォーカスを最初のインタラクティブ要素に自動的に移動
• オーバーレイを閉じた時にフォーカスをトリガー要素に自動的に復元
• Tabキーをオーバーレイ内で循環
• Escキーでオーバーレイを自動的に閉じる
• aria-labelledbyをDialogTitleに自動的に関連付け
• aria-describedbyをDialogDescriptionに自動的に関連付け
オーバーレイを閉じた後、フォーカスはどこに復元すべきですか?
フォーカスはトリガー要素(オーバーレイを開いたボタン)に復元すべきです。これはWCAGの明確な要求です。トリガー要素が削除された場合(例えば削除操作)、フォーカスは論理的な次の要素、例えばリストの前のレコードに復元すべきです。
オーバーレイのアニメーションでフォーカス設定が失敗した場合はどうしますか?
アニメーションが開始した時、オーバーレイがまだ完全に表示されていない可能性があり、フォーカス設定が失敗します。解決策はアニメーションが完了した後にフォーカスを設定し、animationendイベントを監視することです。Radix UIはこの問題を自動的に処理します。
スクリーンリーダーにオーバーレイ内容を読み上げさせる方法は?
DialogTitleとDialogDescriptionが両方設定されていることを確認してください。shadcn/uiはaria-labelledbyとaria-describedbyを自動的に関連付けます。オーバーレイに重要な提示内容がある場合、コンテナにtabindex="0"を追加し、フォーカスが最初にコンテナに置かれ、スクリーンリーダーが全内容を最初に読み上げるようにします。

7 min read · 公開日: 2026年3月29日 · 更新日: 2026年3月29日

コメント

GitHubアカウントでログインしてコメントできます

関連記事