汎用的なModal Dialogコンポーネントです。
Source Code
<button type="button" data-ui-modal-open="basic-modal">基本モーダルを開く</button>
<ui-modal class="ui:modal" id="basic-modal" aria-labelledby="basic-modal-title">
<header class="ui:modal__header">
<h2 id="basic-modal-title">お知らせ</h2>
<button type="button" data-ui-modal-close aria-label="閉じる">×</button>
<div class="ui:modal__body">
<p>本日 22:00 〜 23:00 にメンテナンスを実施します。</p>
<footer class="ui:modal__footer">
<button type="button" data-ui-modal-close>了解</button>
<form method="dialog"> を使うと、submit ボタンの value が <dialog>.returnValue に入り、ui-modal:close の detail.returnValue で取得できます。
Source Code
<button type="button" data-ui-modal-open="confirm-modal">削除する</button>
<ui-modal class="ui:modal" id="confirm-modal" aria-labelledby="confirm-modal-title">
<header class="ui:modal__header">
<h2 id="confirm-modal-title">本当に削除しますか?</h2>
<div class="ui:modal__body">
<footer class="ui:modal__footer">
<button type="submit" value="cancel">キャンセル</button>
<button type="submit" value="confirm">削除する</button>
| 属性 | 要素 | 値 | 効果 |
|---|
data-ui-modal-open | 任意 | 対象 modal の id | クリックで対応する <ui-modal> を開く |
data-ui-modal-close | 任意 | - | <ui-modal> 内のクリックでそのモーダルを閉じる |
| キー | 動作 |
|---|
| Escape Esc Escape Esc | モーダルを閉じる |
| Tab Tab Tab Tab | モーダル内のフォーカス移動(外には出ない) |
| 経路 | 仕組み |
|---|
| Escape Esc Escape Esc キー | ネイティブ <dialog> |
data-ui-modal-close 付き要素のクリック | <ui-modal> 内 delegate |
| モーダル外側(backdrop)のクリック | dialog の padding:0 + e.target === dialog 判定で light-dismiss |
<form method="dialog"> の submit | ネイティブ |
JS から modalElement.close(returnValue?) | API |
| 要件 | 対応 |
|---|
role="dialog" / aria-modal | <dialog>.showModal() が ネイティブ適用 |
| ラベル | 著者が <ui-modal aria-labelledby="..."> 等を指定 (必須推奨) |
| フォーカストラップ | ネイティブ |
| 開く前の要素へのフォーカス復帰 | ネイティブ |
| Escape Esc Escape Esc 閉じ | ネイティブ |
| 背景スクロール抑止 | <html> の overflow:hidden を退避 / 復元 |
<ui-modal> は Light DOM の Web Component ですが、ユーザーアクションによって表示される性質上、JS upgrade 前は 完全に非表示 にしています。
<button type="button" data-ui-modal-open="my-modal">開く</button>
<!-- モーダル本体 (どこに書いても OK) -->
<ui-modal class="ui:modal" id="my-modal" aria-labelledby="my-modal-title">
<header class="ui:modal__header">
<h2 id="my-modal-title">タイトル</h2>
<button type="button" data-ui-modal-close aria-label="閉じる">×</button>
<div class="ui:modal__body">
<footer class="ui:modal__footer">
<button type="button" data-ui-modal-close>キャンセル</button>
<button type="submit">送信</button>
- ルート:
<ui-modal class="ui:modal" id="..."> カスタム要素 (id 必須、トリガーが参照する)
- ラベル:
aria-labelledby または aria-label を <ui-modal> に 付与すれば内部の <dialog> に転写されます
- 補足説明 (任意):
aria-describedby も <ui-modal> に付与すれば内部の <dialog> に転写されます
- 推奨子要素:
.ui:modal__header / .ui:modal__body / .ui:modal__footer (任意、共通スタイル用)
<ui-modal class="ui:modal" id="my-modal" aria-labelledby="my-modal-title" data-ui-modal-ready>
<!-- JS が動的に挿入。著者が書いた children は全てこの中に移動される -->
<dialog aria-labelledby="my-modal-title">
<header class="ui:modal__header">...</header>
<div class="ui:modal__body">...</div>
<footer class="ui:modal__footer">...</footer>
import type { UiModalElement } from "@packages/ui/modal";
const $modal = document.querySelector<UiModalElement>("#my-modal");
$modal?.open(); // 開く (programmatic、トリガーなし)
$modal?.close("done"); // returnValue 付きで閉じる
console.log($modal?.isOpen); // 開閉状態
| イベント | 発火元 | cancelable | bubbles | detail |
|---|
ui-modal:beforeopen | <ui-modal> | ✅ | ✅ | { $trigger: HTMLElement | null } |
ui-modal:open | <ui-modal> | ❌ | ✅ | { $trigger: HTMLElement | null } |
ui-modal:close | <ui-modal> | ❌ | ✅ | { returnValue: string } |
import type { ModalOpenDetail, ModalCloseDetail } from "@packages/ui/modal";
// open をキャンセルする例 (例: 未保存の編集を確認)
document.addEventListener("ui-modal:beforeopen", (e) => {
document.addEventListener("ui-modal:close", (e) => {
console.log("closed with:", e.detail.returnValue);
$trigger は data-ui-modal-open 付きの要素 (クリックで開かれた場合)。element.open() を直接呼んだ場合は null。