日本語ドキュメント / 重なる表示とダイアログ / Dropdown
ふわっと大きくなって開くメニューです。見出し、区切り線、赤い危険な項目が入っています。
この部品は 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手動で入れる場合:
tokens.cssをページで一度だけ読み込む- CSS タブの中身を、自分のスタイルシートに貼る
- 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)・読むトークン・使い方の例・関連する部品
この訳は、英語版の 2026-11-07 の内容をもとにしています。英語版の文が変わると、テストで知らせます。