日本語ドキュメント / 重なる表示とダイアログ / Modal

Modal

モーダルbetaEnglish →

ブラウザ標準の 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

手動で入れる場合:

  1. tokens.css をページで一度だけ読み込む
  2. CSS タブの中身を、自分のスタイルシートに貼る
  3. 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)・読むトークン・使い方の例・関連する部品

英語版の Modal ページで見る →

この訳は、英語版の 2026-11-07 の内容をもとにしています。英語版の文が変わると、テストで知らせます。

    Highlight a part to see it live.