日本語ドキュメント / 重なる表示とダイアログ / Modal
ブラウザ標準の dialog 要素でできています。背景のぼかしと、Esc で閉じる動きは標準のままです。
この部品は Button も使います。その CSS も一緒に読み込んでください。
<button type="button" class="btn btn-pri btn-md" data-modal-open>Open modal</button>
<dialog class="kmodal" data-modal>
<div class="modal-in">
<h3>Send this estimate?</h3>
<p>Once sent, it can't be undone. Check the recipient and the amount first.</p>
<div class="row">
<button type="button" class="btn btn-sec btn-md" data-modal-close>Cancel</button>
<button type="button" class="btn btn-pri btn-md" data-modal-close>Send</button>
</div>
</div>
</dialog>.kmodal{border:0;border-radius:var(--k-radius-xl);padding:0;max-width:420px;width:calc(100% - 40px);box-shadow:var(--k-shadow-modal);background:var(--k-bg);color:var(--k-ink);font-family:var(--k-font)}
.kmodal::backdrop{background:var(--k-backdrop);backdrop-filter:blur(3px)}
.modal-in{padding:var(--k-space-6)}
.modal-in h3{font-size:var(--k-text-lg);font-weight:600}
.modal-in p{margin-top:var(--k-space-2);font-size:var(--k-text-base);color:var(--k-ink-2);line-height:var(--k-lh-loose);letter-spacing:.012em}
.modal-in .row{display:flex;gap:var(--k-space-2);justify-content:flex-end;margin-top:var(--k-space-5)}
// KATA Modal — independent native modal instances.
(() => {
let nextId = 0;
function ensureId(element, suffix) {
if (element.id) return element.id;
let id;
do { id = `kata-modal-${++nextId}-${suffix}`; } while (document.getElementById(id));
element.id = id;
return id;
}
const dialogs = [...document.querySelectorAll('[data-modal]')];
const triggers = [...document.querySelectorAll('[data-modal-open]')];
function targetOf(trigger) {
const controls = trigger.getAttribute('aria-controls');
if (controls) {
const target = dialogs.find(dialog => dialog.id === controls);
if (target) return target;
}
return dialogs.find(dialog => trigger.compareDocumentPosition(dialog) & Node.DOCUMENT_POSITION_FOLLOWING) || dialogs[dialogs.length - 1];
}
dialogs.forEach(dialog => {
if (dialog.hasAttribute('data-kata-ready') || !(dialog instanceof HTMLDialogElement)) return;
dialog.setAttribute('data-kata-ready', '');
const id = ensureId(dialog, 'dialog');
const heading = dialog.querySelector('h1,h2,h3,h4,h5,h6');
const description = dialog.querySelector('p');
if (!dialog.hasAttribute('aria-label') && !dialog.hasAttribute('aria-labelledby') && heading) dialog.setAttribute('aria-labelledby', ensureId(heading, 'title'));
if (!dialog.hasAttribute('aria-describedby') && description) dialog.setAttribute('aria-describedby', ensureId(description, 'description'));
if (!dialog.hasAttribute('tabindex')) dialog.tabIndex = -1;
let opener = null;
let composing = false;
function focusables() {
return [...dialog.querySelectorAll('button,a[href],input,select,textarea,[tabindex]')].filter(element => element.tabIndex >= 0 && !element.disabled && !element.closest('[inert]') && element.getClientRects().length && getComputedStyle(element).visibility !== 'hidden');
}
// Native close is immediate; it never waits for a CSS end event.
function close() { composing = false; if (dialog.open) dialog.close(); }
triggers.filter(trigger => targetOf(trigger) === dialog).forEach(trigger => {
ensureId(trigger, 'trigger');
trigger.setAttribute('aria-controls', id);
trigger.setAttribute('aria-haspopup', 'dialog');
trigger.addEventListener('click', () => {
if (dialog.open) return;
composing = false;
opener = trigger;
dialog.showModal();
(focusables().find(element => element.hasAttribute('autofocus')) || focusables()[0] || dialog).focus();
});
});
dialog.querySelectorAll('[data-modal-close]').forEach(button => button.addEventListener('click', close));
dialog.addEventListener('compositionstart', () => { composing = true; });
dialog.addEventListener('compositionend', () => { composing = false; });
dialog.addEventListener('cancel', event => { if (composing) event.preventDefault(); });
dialog.addEventListener('keydown', event => {
if (composing || event.isComposing || event.keyCode === 229) {
if (event.key === 'Escape') event.preventDefault();
return;
}
if (event.key !== 'Tab') return;
const items = focusables();
const first = items[0], last = items[items.length - 1];
if (!first) { event.preventDefault(); dialog.focus(); return; }
if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog)) { event.preventDefault(); first.focus(); }
});
// A click on the backdrop lands on the dialog element outside its box.
dialog.addEventListener('click', event => {
if (event.target !== dialog) return;
const rect = dialog.getBoundingClientRect();
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) close();
});
dialog.addEventListener('close', () => {
if (dialog.open) return; // Ignore a queued close event from an earlier opening.
composing = false;
if (opener?.isConnected && !opener.disabled && !document.querySelectorAll('dialog[open]').length) opener.focus();
});
});
})();
導入
npx @kajisho5/kataui add modal手動で入れる場合:
tokens.cssをページで一度だけ読み込む- CSS タブの中身を、自分のスタイルシートに貼る
- HTML タブの中身を、使いたい場所に貼る。 この部品にはスクリプトがあります。HTML のあとで読み込むと、自分で動き始めます。
使いどころ
流れを止めて確認してもらうときに使います。伝える内容は、決めてほしいこと1つに絞ります。
アクセシビリティ
ブラウザ標準の <dialog> を showModal で開きます。Tab と Shift + Tab は、ダイアログの中だけを回ります。Escape、背景のクリック、[data-modal-close] のボタンで閉じ、開いたボタンにフォーカスが戻ります。最初の見出しがダイアログの名前に(aria-labelledby)、最初の段落が説明に(aria-describedby)なります。id は自動で作り、もとからある id はそのまま残します。ページの中のダイアログはそれぞれ別々に動き、スクリプトを2回読み込んでも安全です。日本語入力で変換している間は、Escape を受け付けません。主な操作のボタンには「OK」ではなく、することを表す動詞を書いてください。スクリーンリーダーでの確認を記録するまで Beta のままです。
| キー | 動き |
|---|---|
| Tab / Shift + Tab | ダイアログの中で移動 |
| Esc | 閉じて、開いたボタンへ戻る |
英語版にだけある情報
静的チェックの結果・部品の構造(Anatomy)・読むトークン・使い方の例・関連する部品
この訳は、英語版の 2026-11-07 の内容をもとにしています。英語版の文が変わると、テストで知らせます。