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

Dropdown

ドロップダウンbetaEnglish →

ふわっと大きくなって開くメニューです。見出し、区切り線、赤い危険な項目が入っています。

見本

この部品は Button も使います。その CSS も一緒に読み込んでください。

<span class="dd">
  <button type="button" class="btn btn-sec btn-md" data-dd-trigger>Actions <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
  <span class="menu" data-dd-menu>
    <span class="mh">Project</span>
    <a href="#">Edit</a><a href="#">Duplicate</a><a href="#">Copy share link</a>
    <hr>
    <a href="#" class="danger">Delete</a>
  </span>
</span>
.dd{position:relative;display:inline-block}
/* A closed menu is hidden from Tab and assistive tech once its fade-out ends. */
.menu{position:absolute;top:calc(100% + 6px);inset-inline-start:0;min-width:196px;background:var(--k-bg);border:1px solid var(--k-line);border-radius:var(--k-radius-lg);box-shadow:var(--k-shadow-pop);padding:var(--k-space-1);opacity:0;visibility:hidden;transform:translateY(-4px) scale(.98);pointer-events:none;transition:opacity var(--k-dur-fast),transform var(--k-dur-fast),visibility 0s linear var(--k-dur-fast);z-index:20}
.menu.open{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition-delay:0s}
.menu a{display:flex;gap:var(--k-space-2);align-items:center;padding:var(--k-space-2) var(--k-space-2);border-radius:var(--k-radius-md);color:var(--k-ink);text-decoration:none;font-size:var(--k-text-base);font-family:var(--k-font)}
.menu a:hover,.menu a:focus-visible{background:var(--k-bg-soft)}
.menu a:active{background:var(--k-line)}
.menu a.danger{color:var(--k-danger-ink)}
.menu a.danger:hover,.menu a.danger:focus-visible{background:var(--k-danger-bg)}
/* Disabled items stay readable, are skipped by the arrow keys and do not react to the pointer. */
.menu a[aria-disabled="true"]{color:var(--k-ink-3);pointer-events:none}
.menu hr{border:0;border-top:1px solid var(--k-line);margin:var(--k-space-1) var(--k-space-1)}
.menu .mh{display:block;font-size:var(--k-text-xs);color:var(--k-ink-3);padding:var(--k-space-1) var(--k-space-2) var(--k-space-1);letter-spacing:.06em}
// KATA Dropdown — menu button (WAI-ARIA APG pattern), independent instances.
(() => {
  let nextId = 0;
  function ensureId(element, suffix) {
    if (element.id) return element.id;
    let id;
    do { id = `kata-dropdown-${++nextId}-${suffix}`; } while (document.getElementById(id));
    element.id = id;
    return id;
  }
  const ITEMS = 'a[href],button,[role="menuitem"]';
  const instances = [];
  function emit(root, open) { root.dispatchEvent(new CustomEvent('kata-dropdown-toggle', { bubbles: true, detail: { open } })); }
  document.querySelectorAll('[data-dd-trigger]').forEach(trigger => {
    const root = trigger.closest('.dd') || trigger.parentElement;
    const menu = root && root.querySelector('[data-dd-menu]');
    if (!menu || root.hasAttribute('data-kata-ready')) return;
    root.setAttribute('data-kata-ready', '');
    const menuId = ensureId(menu, 'menu');
    trigger.setAttribute('aria-haspopup', 'menu');
    trigger.setAttribute('aria-controls', menuId);
    trigger.setAttribute('aria-expanded', 'false');
    menu.setAttribute('role', 'menu');
    if (!menu.hasAttribute('aria-label') && !menu.hasAttribute('aria-labelledby')) menu.setAttribute('aria-labelledby', ensureId(trigger, 'trigger'));
    const items = () => [...menu.querySelectorAll(ITEMS)].filter(item => !item.hidden && item.getAttribute('aria-disabled') !== 'true' && !item.disabled);
    menu.querySelectorAll(ITEMS).forEach(item => { item.setAttribute('role', 'menuitem'); item.tabIndex = -1; });
    menu.querySelectorAll('hr').forEach(hr => hr.setAttribute('role', 'separator'));
    menu.querySelectorAll('.mh').forEach(heading => heading.setAttribute('role', 'presentation'));
    const isOpen = () => menu.classList.contains('open');
    function open(focus) {
      if (!isOpen()) {
        instances.forEach(other => { if (other.root !== root) other.close(false); });
        menu.classList.add('open');
        trigger.setAttribute('aria-expanded', 'true');
        emit(root, true);
      }
      const list = items();
      const target = focus === 'last' ? list[list.length - 1] : list[0];
      if (target) target.focus();
    }
    function close(returnFocus) {
      if (!isOpen()) return;
      menu.classList.remove('open');
      trigger.setAttribute('aria-expanded', 'false');
      if (returnFocus) trigger.focus();
      emit(root, false);
    }
    instances.push({ root, close });
    trigger.addEventListener('click', () => { if (isOpen()) close(true); else open('first'); });
    trigger.addEventListener('keydown', event => {
      if (event.isComposing || event.keyCode === 229) return;
      if (event.key === 'ArrowDown') { event.preventDefault(); open('first'); }
      else if (event.key === 'ArrowUp') { event.preventDefault(); open('last'); }
      else if (event.key === 'Escape' && isOpen()) { event.preventDefault(); close(true); }
    });
    menu.addEventListener('keydown', event => {
      if (event.isComposing || event.keyCode === 229) return;
      const list = items();
      const index = list.indexOf(document.activeElement);
      let next = null;
      if (event.key === 'ArrowDown') next = list[(index + 1) % list.length];
      else if (event.key === 'ArrowUp') next = list[(index - 1 + list.length) % list.length];
      else if (event.key === 'Home' || event.key === 'PageUp') next = list[0];
      else if (event.key === 'End' || event.key === 'PageDown') next = list[list.length - 1];
      else if (event.key === 'Escape') { event.preventDefault(); close(true); return; }
      else if (event.key === 'Tab') { close(false); return; }
      else if (event.key === ' ' && document.activeElement?.matches('a[href]')) { event.preventDefault(); document.activeElement.click(); return; }
      if (next) { event.preventDefault(); next.focus(); }
    });
    menu.addEventListener('click', event => { if (event.target.closest(ITEMS)) close(true); });
    // A null relatedTarget (window switch, removed node) never closes the menu.
    root.addEventListener('focusout', event => { if (event.relatedTarget && !root.contains(event.relatedTarget)) close(false); });
  });
  // One outside-press and one window-blur listener per script load, shared by its instances.
  if (!instances.length) return;
  document.addEventListener('pointerdown', event => { instances.forEach(instance => { if (!instance.root.contains(event.target)) instance.close(false); }); });
  window.addEventListener('blur', () => instances.forEach(instance => instance.close(false)));
})();

導入

npx @kajisho5/kataui add dropdown

手動で入れる場合:

  1. tokens.css をページで一度だけ読み込む
  2. CSS タブの中身を、自分のスタイルシートに貼る
  3. HTML タブの中身を、使いたい場所に貼る。 この部品にはスクリプトがあります。HTML のあとで読み込むと、自分で動き始めます。

使いどころ

1つのボタンの下に操作をまとめます。取り消せない操作は最後に置き、区切り線の下に赤で表示します。

アクセシビリティ

WAI-ARIA APG のメニューボタンの形です。ボタンには aria-haspopup="menu"、aria-expanded、aria-controls を付けます。メニューは role="menu" で、ボタンの文が名前になります。項目は menuitem、区切り線は separator です。Enter・Space・↓ で開くと最初の項目へ、↑ で開くと最後の項目へ移ります。↑/↓ は端から端へ回り、Home/End で最初と最後へ飛び、押せない項目(aria-disabled)は飛ばします。Escape か項目を選ぶとメニューが閉じ、ボタンへフォーカスが戻ります。Tab、メニューの外を押す、ウィンドウから離れる、でも閉じます。1つのメニューを開くと、ほかのメニューは閉じます。ページの中のメニューはそれぞれ別々に動き、スクリプトを2回読み込んでも安全です。閉じている項目は Tab で止まりません。スクリーンリーダーでの確認を記録するまで Beta のままです。

キー動き
Enter / Space / ↓開いて最初の項目へ
↑開いて最後の項目へ
↑ / ↓項目を移る(端で回る)
Home / End最初・最後の項目へ
Esc閉じて、ボタンへ戻る

英語版にだけある情報

静的チェックの結果・部品の構造(Anatomy)・読むトークン・使い方の例・関連する部品

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

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

    Highlight a part to see it live.