Basic Dropdown
A menu button with the full keyboard model - arrows, Home/End, Escape, focus restore.
6 個のフレームワーク中級
A menu with a glyph on every row - icons as scanning aids, labels as the names.
<!--
Every icon is aria-hidden and every row still has its text label. An icon
beside a word is a scanning aid for people who already know the menu; it is
never the name of the action. Strip the labels and you have a row of glyphs
that announce as "button, button, button".
The glyphs are drawn with stroke="currentColor", so they inherit the row's
colour - including the hover and dark-mode shifts - instead of needing their
own dark: variants and drifting out of step with the text beside them.
-->
<div class="idd">
<button
class="idd__trigger"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="idd-menu"
>
Manage
<svg class="idd__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<ul class="idd__menu" id="idd-menu" role="menu" aria-label="Manage" hidden>
<li role="none">
<button class="idd__item" type="button" role="menuitem">
<svg class="idd__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
Edit
</button>
</li>
<li role="none">
<button class="idd__item" type="button" role="menuitem">
<svg class="idd__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<rect x="9" y="9" width="12" height="12" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Duplicate
</button>
</li>
<li role="none">
<button class="idd__item" type="button" role="menuitem">
<svg class="idd__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" />
</svg>
Export
</button>
</li>
<li role="none">
<button class="idd__item idd__item--danger" type="button" role="menuitem">
<svg class="idd__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
</svg>
Delete
</button>
</li>
</ul>
</div>
<script>
document.querySelectorAll('.idd').forEach(function (root) {
var trigger = root.querySelector('.idd__trigger');
var menu = root.querySelector('.idd__menu');
var items = Array.prototype.slice.call(root.querySelectorAll('.idd__item'));
function setOpen(open) {
menu.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
}
function close() {
setOpen(false);
trigger.focus();
}
trigger.addEventListener('click', function () {
var next = trigger.getAttribute('aria-expanded') !== 'true';
setOpen(next);
if (next && items[0]) items[0].focus();
});
root.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
close();
return;
}
if (event.key === 'Home') {
event.preventDefault();
items[0].focus();
return;
}
if (event.key === 'End') {
event.preventDefault();
items[items.length - 1].focus();
return;
}
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
var index = items.indexOf(document.activeElement);
if (index === -1) return;
event.preventDefault();
var next = event.key === 'ArrowDown' ? index + 1 : index - 1;
items[(next + items.length) % items.length].focus();
});
items.forEach(function (item) {
item.addEventListener('click', close);
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
});
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
label必須 | string | - | 読み込み中に読み上げられるアクセシブルなラベル。 |
items必須 | MenuItem[] | - | レンダリングする項目の配列。 |
icon | ReactNode | - | コンテンツと並べて表示されるアイコン要素。 |
danger | boolean | false | 項目を破壊的な操作として示し、警告として見えるようにします。 |
onSelect | (id: string) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
Every icon is `aria-hidden` and every row keeps its text label. An icon beside a word is a scanning aid for people who already know the menu; it is never the name of the action. Strip the labels and you have a row of glyphs announcing "button, button, button". Draw them with `stroke="currentColor"` so they inherit the row's colour - including hover and the dark-mode shift - instead of carrying their own `dark:` variants and drifting out of step with the text beside them. At rest the glyph sits a step quieter than its label (`text-gray-500`) and snaps to `currentColor` on hover: the word is the action, the icon is the hint. `danger` is a flag on the item rather than a colour at the call site, so danger looks the same everywhere by construction.