Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z"/><path d="m13.5 6.5 4 4"/></symbol>
  <symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="12" height="12" rx="2"/><path d="M15.5 8.5V5a1.5 1.5 0 0 0-1.5-1.5H5A1.5 1.5 0 0 0 3.5 5v9A1.5 1.5 0 0 0 5 15.5h3.5"/></symbol>
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>
  <symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></symbol>
  <symbol id="i-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3 10h18M7 15h3"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10.5l5 5 5-5M5 19.5h14"/></symbol>
  <symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V5M7 9.5l5-5 5 5M5 19.5h14"/></symbol>
  <symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8.5V3.5h10v5"/><rect x="3.5" y="8.5" width="17" height="8" rx="2"/><path d="M7 14h10v6.5H7z"/></symbol>
  <symbol id="i-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="16" rx="2.5"/><path d="M9.2 4v16M14.8 4v16"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-form" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4.5" y="3.5" width="15" height="17" rx="2"/><path d="M8.5 8h7M8.5 12h7M8.5 16h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>
<div><span class="lbl">Icons, shortcuts and a danger item</span>
<div class="stage" style="--stage-h:430px">
<div class="am">
<button class="am-btn" id="am-a-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-a">Actions<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="am-menu" role="menu" id="am-a" aria-labelledby="am-a-btn" hidden>
<button type="button" class="am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+E"><svg aria-hidden="true"><use href="#i-pencil"/></svg><span>Edit</span><kbd class="am-kbd" aria-hidden="true">⌘E</kbd></button>
<button type="button" class="am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+D"><svg aria-hidden="true"><use href="#i-copy"/></svg><span>Duplicate</span><kbd class="am-kbd" aria-hidden="true">⌘D</kbd></button>
<button type="button" class="am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+S"><svg aria-hidden="true"><use href="#i-share"/></svg><span>Share…</span><kbd class="am-kbd" aria-hidden="true">⇧⌘S</kbd></button>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Move to folder</button>
<div class="am-sep" role="separator"></div>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-archive"/></svg>Archive</button>
<div class="am-sep" role="separator"></div>
<button type="button" class="am-item danger" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+Backspace"><svg aria-hidden="true"><use href="#i-trash"/></svg><span>Delete</span><kbd class="am-kbd" aria-hidden="true">⌘⌫</kbd></button>
</div>
</div>
</div></div>
<div><span class="lbl">Group headers and a disabled item</span>
<div class="stage" style="--stage-h:430px">
<div class="am">
<button class="am-btn" id="am-b-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-b">Invoice INV-2041<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="am-menu" role="menu" id="am-b" aria-labelledby="am-b-btn" hidden>
<div role="group" aria-labelledby="am-b-h1">
<div class="am-head" id="am-b-h1">Send</div>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-mail"/></svg>Email to Harbor & Pine</button>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-link"/></svg>Copy payment link</button>
</div>
<div class="am-sep" role="separator"></div>
<div role="group" aria-labelledby="am-b-h2">
<div class="am-head" id="am-b-h2">Payments</div>
<button type="button" class="am-item" role="menuitem" tabindex="-1" aria-disabled="true" aria-labelledby="am-b-t3" aria-describedby="am-b-d3"><svg aria-hidden="true"><use href="#i-card"/></svg><span id="am-b-t3">Record payment</span><span class="am-hint" id="am-b-d3">Paid 12 Sep</span></button>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-undo"/></svg>Issue refund</button>
</div>
<div class="am-sep" role="separator"></div>
<button type="button" class="am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-download"/></svg>Download PDF</button>
<button type="button" class="am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+P"><svg aria-hidden="true"><use href="#i-printer"/></svg><span>Print</span><kbd class="am-kbd" aria-hidden="true">⌘P</kbd></button>
</div>
</div>
</div></div>
<div><span class="lbl">Items with descriptions</span>
<div class="stage" style="--stage-h:430px">
<div class="am">
<button class="am-btn primary" id="am-c-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-c"><svg aria-hidden="true"><use href="#i-plus"/></svg>New<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="am-menu wide" role="menu" id="am-c" aria-labelledby="am-c-btn" hidden>
<button type="button" class="am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t1" aria-describedby="am-c-d1"><span class="am-tile"><svg aria-hidden="true"><use href="#i-board"/></svg></span><span><span class="am-title" id="am-c-t1">Project</span><span class="am-desc" id="am-c-d1">A board with tasks, owners and due dates</span></span></button>
<button type="button" class="am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t2" aria-describedby="am-c-d2"><span class="am-tile"><svg aria-hidden="true"><use href="#i-file"/></svg></span><span><span class="am-title" id="am-c-t2">Document</span><span class="am-desc" id="am-c-d2">Long-form notes the team can comment on</span></span></button>
<button type="button" class="am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t3" aria-describedby="am-c-d3"><span class="am-tile"><svg aria-hidden="true"><use href="#i-form"/></svg></span><span><span class="am-title" id="am-c-t3">Form</span><span class="am-desc" id="am-c-d3">Collect replies straight into a table</span></span></button>
<div class="am-sep" role="separator"></div>
<button type="button" class="am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t4" aria-describedby="am-c-d4"><span class="am-tile"><svg aria-hidden="true"><use href="#i-upload"/></svg></span><span><span class="am-title" id="am-c-t4">Import from CSV</span><span class="am-desc" id="am-c-d4">Up to 10,000 rows, mapped column by column</span></span></button>
</div>
</div>
</div></div>
CSS
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* Action menus: a menu button (aria-haspopup="menu") and a role="menu" list that the script shows and hides. */
.am{--am-ink:#1d2130;--am-muted:#5c6273;--am-line:#e6e8ee;--am-hover:#f1f2f7;--am-accent:#4f46e5;--am-accent-dark:#4338ca;--am-tint:#eef0ff;
  --am-danger:#c42b2b;--am-danger-bg:#fdf1f1;position:relative;display:inline-block;font-size:.875rem}
.am-btn{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 12px 0 14px;border:1px solid #d3d7e0;border-radius:10px;background:#fff;
  color:var(--am-ink);font:inherit;font-weight:600;cursor:pointer;box-shadow:0 1px 2px rgba(16,24,40,.06);transition:background-color .15s,border-color .15s}
.am-btn:hover{background:#f7f8fb}
.am-btn[aria-expanded="true"]{background:var(--am-hover);border-color:#bfc4d1}
.am-btn svg{width:16px;height:16px;flex:none;color:var(--am-muted);transition:rotate .2s}
.am-btn[aria-expanded="true"] .am-chev{rotate:180deg}
.am-btn.primary{padding-left:12px;border-color:var(--am-accent);background:var(--am-accent);color:#fff;box-shadow:0 1px 2px rgba(16,24,40,.1),0 8px 18px -8px rgba(79,70,229,.7)}
.am-btn.primary svg{color:currentColor}
.am-btn.primary:hover,.am-btn.primary[aria-expanded="true"]{background:var(--am-accent-dark);border-color:var(--am-accent-dark)}
.am-btn:focus-visible{outline:2px solid var(--am-accent);outline-offset:2px}

.am-menu{position:absolute;z-index:20;top:calc(100% + 6px);left:0;min-width:236px;padding:6px;background:#fff;border:1px solid var(--am-line);border-radius:12px;
  box-shadow:0 1px 2px rgba(16,24,40,.04),0 14px 34px -10px rgba(16,24,40,.22);transform-origin:top left;animation:am-in .14s ease-out}
.am-menu[hidden]{display:none}
.am-menu.wide{width:312px;max-width:calc(100vw - 72px)}
@keyframes am-in{from{opacity:0;translate:0 -4px;scale:.98}}
.am-item{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:none;color:var(--am-ink);
  font:inherit;font-weight:500;line-height:1.4;text-align:left;text-decoration:none;white-space:nowrap;cursor:pointer}
.am-item>svg{width:16px;height:16px;flex:none;color:var(--am-muted)}
.am-item:hover,.am-item:focus{background:var(--am-hover);outline:none}
.am-item:focus-visible{outline:2px solid var(--am-accent);outline-offset:-2px}
.am-kbd,.am-hint{margin-left:auto;padding-left:18px;color:var(--am-muted);font-size:.75rem}
.am-kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:500}
.am-item.danger,.am-item.danger>svg{color:var(--am-danger)}
.am-item.danger:hover,.am-item.danger:focus{background:var(--am-danger-bg)}
.am-item[aria-disabled="true"]{color:#8b90a0;cursor:not-allowed}
.am-item[aria-disabled="true"]:hover{background:none}
.am-sep{height:1px;margin:6px -6px;background:var(--am-line)}
.am-head{padding:8px 10px 4px;color:var(--am-muted);font-size:.6875rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
/* items with a description */
.am-item.rich{align-items:flex-start;gap:12px;padding:10px;white-space:normal}
.am-tile{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:var(--am-tint);color:var(--am-accent)}
.am-tile svg{width:18px;height:18px}
.am-title{display:block;font-weight:600}
.am-desc{display:block;margin-top:1px;color:var(--am-muted);font-size:.8rem;font-weight:400;line-height:1.4}
@media (prefers-reduced-motion:reduce){.am-menu{animation:none}.am-btn,.am-btn svg{transition:none}}
JavaScript
// Menu button (WAI-ARIA APG pattern). A trigger has aria-haspopup="menu", aria-expanded and aria-controls="<menu id>".
// Click, Enter, Space or ArrowDown opens the menu (ArrowUp opens it on the last item). Inside: ArrowUp/ArrowDown move,
// Home/End jump, typing a letter jumps to the next item that starts with it, Esc closes and returns focus to the trigger,
// Tab or a click outside closes. Choosing an item closes the menu; checkbox items stay open so you can tick several.
(() => {
  const OPEN = '[aria-haspopup="menu"][aria-expanded="true"]';
  const menuOf = t => document.getElementById(t.getAttribute('aria-controls'));
  const ownerOf = m => m && document.querySelector(`[aria-controls="${m.id}"][aria-expanded="true"]`);
  const itemsOf = m => [...m.querySelectorAll('[role^="menuitem"]')]
    .filter(i => i.closest('[role="menu"]') === m && i.getAttribute('aria-disabled') !== 'true');

  function close(t, focus) {
    const m = menuOf(t);
    m.querySelectorAll(OPEN).forEach(s => close(s));
    t.setAttribute('aria-expanded', 'false');
    m.hidden = true;
    if (focus) t.focus();
  }
  function open(t, at) {
    const level = t.closest('[role="menu"]');
    document.querySelectorAll(OPEN).forEach(o => { if (o !== t && o.closest('[role="menu"]') === level) close(o); });
    t.setAttribute('aria-expanded', 'true');
    menuOf(t).hidden = false;
    if (at !== undefined) itemsOf(menuOf(t)).at(at)?.focus();
  }
  const closeAll = target => document.querySelectorAll(OPEN).forEach(t => {
    if (!t.closest('[role="menu"]') && !t.contains(target) && !menuOf(t).contains(target)) close(t);
  });

  document.addEventListener('click', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t) {
      if (!t.closest('[role="menu"]') && t.getAttribute('aria-expanded') === 'true') close(t);
      else open(t, e.detail === 0 ? 0 : undefined); // keyboard clicks (Enter/Space) move focus into the menu
      return;
    }
    const item = e.target.closest('[role^="menuitem"]');
    if (item && item.getAttribute('aria-disabled') !== 'true') {
      const role = item.getAttribute('role');
      if (role === 'menuitemcheckbox') { item.setAttribute('aria-checked', item.getAttribute('aria-checked') !== 'true'); return; }
      if (role === 'menuitemradio') {
        const set = item.closest('[role="group"]') || item.closest('[role="menu"]');
        set.querySelectorAll('[role="menuitemradio"]').forEach(r => r.setAttribute('aria-checked', r === item));
      }
      let t = ownerOf(item.closest('[role="menu"]'));
      while (t && t.closest('[role="menu"]')) t = ownerOf(t.closest('[role="menu"]'));
      if (t) close(t, true);
      return;
    }
    closeAll(e.target);
  });

  document.addEventListener('keydown', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t && !t.closest('[role="menu"]')) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(t, e.key === 'ArrowDown' ? 0 : -1); }
      else if (e.key === 'Escape' && t.getAttribute('aria-expanded') === 'true') close(t);
      return;
    }
    const m = e.target.closest('[role="menu"]');
    if (!m) return;
    const items = itemsOf(m), i = items.indexOf(e.target), owner = ownerOf(m);
    let next;
    switch (e.key) {
      case 'ArrowDown': next = items[(i + 1) % items.length]; break;
      case 'ArrowUp': next = items.at(i <= 0 ? -1 : i - 1); break;
      case 'Home': next = items[0]; break;
      case 'End': next = items.at(-1); break;
      case 'Escape': if (owner) { e.preventDefault(); close(owner, true); } return;
      case 'Tab': closeAll(null); return;
      case ' ': if (e.target.tagName === 'A') { e.preventDefault(); e.target.click(); } return;
      default:
        if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
        next = [...items.slice(i + 1), ...items.slice(0, i + 1)]
          .find(x => x.textContent.trim().toLowerCase().startsWith(e.key.toLowerCase()));
    }
    e.preventDefault();
    next?.focus();
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z"/><path d="m13.5 6.5 4 4"/></symbol>
  <symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="12" height="12" rx="2"/><path d="M15.5 8.5V5a1.5 1.5 0 0 0-1.5-1.5H5A1.5 1.5 0 0 0 3.5 5v9A1.5 1.5 0 0 0 5 15.5h3.5"/></symbol>
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>
  <symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></symbol>
  <symbol id="i-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3 10h18M7 15h3"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10.5l5 5 5-5M5 19.5h14"/></symbol>
  <symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V5M7 9.5l5-5 5 5M5 19.5h14"/></symbol>
  <symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8.5V3.5h10v5"/><rect x="3.5" y="8.5" width="17" height="8" rx="2"/><path d="M7 14h10v6.5H7z"/></symbol>
  <symbol id="i-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="16" rx="2.5"/><path d="M9.2 4v16M14.8 4v16"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-form" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4.5" y="3.5" width="15" height="17" rx="2"/><path d="M8.5 8h7M8.5 12h7M8.5 16h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>
<div><span class="lbl">Icons, shortcuts and a danger item</span>
<div class="stage" style="--stage-h:430px">
<div class="dropdown am">
<button class="btn am-btn" id="am-a-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" aria-haspopup="menu" aria-expanded="false" aria-controls="am-a">Actions<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="dropdown-menu am-menu" role="menu" id="am-a" aria-labelledby="am-a-btn">
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+E"><svg aria-hidden="true"><use href="#i-pencil"/></svg><span>Edit</span><kbd class="am-kbd" aria-hidden="true">⌘E</kbd></button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+D"><svg aria-hidden="true"><use href="#i-copy"/></svg><span>Duplicate</span><kbd class="am-kbd" aria-hidden="true">⌘D</kbd></button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+S"><svg aria-hidden="true"><use href="#i-share"/></svg><span>Share…</span><kbd class="am-kbd" aria-hidden="true">⇧⌘S</kbd></button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Move to folder</button>
<hr class="dropdown-divider">
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-archive"/></svg>Archive</button>
<hr class="dropdown-divider">
<button type="button" class="dropdown-item am-item danger" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+Backspace"><svg aria-hidden="true"><use href="#i-trash"/></svg><span>Delete</span><kbd class="am-kbd" aria-hidden="true">⌘⌫</kbd></button>
</div>
</div>
</div></div>
<div><span class="lbl">Group headers and a disabled item</span>
<div class="stage" style="--stage-h:430px">
<div class="dropdown am">
<button class="btn am-btn" id="am-b-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" aria-haspopup="menu" aria-expanded="false" aria-controls="am-b">Invoice INV-2041<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="dropdown-menu am-menu" role="menu" id="am-b" aria-labelledby="am-b-btn">
<div role="group" aria-labelledby="am-b-h1">
<div class="dropdown-header" id="am-b-h1">Send</div>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-mail"/></svg>Email to Harbor & Pine</button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-link"/></svg>Copy payment link</button>
</div>
<hr class="dropdown-divider">
<div role="group" aria-labelledby="am-b-h2">
<div class="dropdown-header" id="am-b-h2">Payments</div>
<button type="button" class="dropdown-item am-item disabled" role="menuitem" tabindex="-1" aria-disabled="true" aria-labelledby="am-b-t3" aria-describedby="am-b-d3"><svg aria-hidden="true"><use href="#i-card"/></svg><span id="am-b-t3">Record payment</span><span class="am-hint" id="am-b-d3">Paid 12 Sep</span></button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-undo"/></svg>Issue refund</button>
</div>
<hr class="dropdown-divider">
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-download"/></svg>Download PDF</button>
<button type="button" class="dropdown-item am-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+P"><svg aria-hidden="true"><use href="#i-printer"/></svg><span>Print</span><kbd class="am-kbd" aria-hidden="true">⌘P</kbd></button>
</div>
</div>
</div></div>
<div><span class="lbl">Items with descriptions</span>
<div class="stage" style="--stage-h:430px">
<div class="dropdown am">
<button class="btn am-btn primary" id="am-c-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" aria-haspopup="menu" aria-expanded="false" aria-controls="am-c"><svg aria-hidden="true"><use href="#i-plus"/></svg>New<svg class="am-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="dropdown-menu am-menu wide" role="menu" id="am-c" aria-labelledby="am-c-btn">
<button type="button" class="dropdown-item am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t1" aria-describedby="am-c-d1"><span class="am-tile"><svg aria-hidden="true"><use href="#i-board"/></svg></span><span><span class="am-title" id="am-c-t1">Project</span><span class="am-desc" id="am-c-d1">A board with tasks, owners and due dates</span></span></button>
<button type="button" class="dropdown-item am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t2" aria-describedby="am-c-d2"><span class="am-tile"><svg aria-hidden="true"><use href="#i-file"/></svg></span><span><span class="am-title" id="am-c-t2">Document</span><span class="am-desc" id="am-c-d2">Long-form notes the team can comment on</span></span></button>
<button type="button" class="dropdown-item am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t3" aria-describedby="am-c-d3"><span class="am-tile"><svg aria-hidden="true"><use href="#i-form"/></svg></span><span><span class="am-title" id="am-c-t3">Form</span><span class="am-desc" id="am-c-d3">Collect replies straight into a table</span></span></button>
<hr class="dropdown-divider">
<button type="button" class="dropdown-item am-item rich" role="menuitem" tabindex="-1" aria-labelledby="am-c-t4" aria-describedby="am-c-d4"><span class="am-tile"><svg aria-hidden="true"><use href="#i-upload"/></svg></span><span><span class="am-title" id="am-c-t4">Import from CSV</span><span class="am-desc" id="am-c-d4">Up to 10,000 rows, mapped column by column</span></span></button>
</div>
</div>
</div></div>
CSS
/* Bootstrap .dropdown restyled through its own --bs-dropdown-* and --bs-btn-* variables. */
.am{--am-ink:#1d2130;--am-muted:#5c6273;--am-line:#e6e8ee;--am-hover:#f1f2f7;--am-accent:#4f46e5;--am-accent-dark:#4338ca;--am-tint:#eef0ff;
  --am-danger:#c42b2b;--am-danger-bg:#fdf1f1;font-size:.875rem}
.am-btn{--bs-btn-padding-x:14px;--bs-btn-padding-y:0;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-color:var(--am-ink);--bs-btn-bg:#fff;
  --bs-btn-border-color:#d3d7e0;--bs-btn-border-radius:10px;--bs-btn-hover-color:var(--am-ink);--bs-btn-hover-bg:#f7f8fb;--bs-btn-hover-border-color:#d3d7e0;
  --bs-btn-active-color:var(--am-ink);--bs-btn-active-bg:var(--am-hover);--bs-btn-active-border-color:#bfc4d1;
  display:inline-flex;align-items:center;gap:8px;height:40px;padding-right:12px;line-height:1;box-shadow:0 1px 2px rgba(16,24,40,.06)}
.am-btn svg{width:16px;height:16px;flex:none;color:var(--am-muted);transition:rotate .2s}
.am-btn.show .am-chev{rotate:180deg}
.am-btn.primary{--bs-btn-color:#fff;--bs-btn-bg:var(--am-accent);--bs-btn-border-color:var(--am-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--am-accent-dark);
  --bs-btn-hover-border-color:var(--am-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--am-accent-dark);--bs-btn-active-border-color:var(--am-accent-dark);
  padding-left:12px;box-shadow:0 1px 2px rgba(16,24,40,.1),0 8px 18px -8px rgba(79,70,229,.7)}
.am-btn.primary svg{color:currentColor}
.am-btn:focus-visible{outline:2px solid var(--am-accent);outline-offset:2px;box-shadow:none}

.am-menu{--bs-dropdown-min-width:236px;--bs-dropdown-padding-x:6px;--bs-dropdown-padding-y:6px;--bs-dropdown-font-size:.875rem;--bs-dropdown-color:var(--am-ink);
  --bs-dropdown-border-color:var(--am-line);--bs-dropdown-border-radius:12px;--bs-dropdown-divider-bg:var(--am-line);--bs-dropdown-divider-margin-y:6px;
  --bs-dropdown-link-color:var(--am-ink);--bs-dropdown-link-hover-color:var(--am-ink);--bs-dropdown-link-hover-bg:var(--am-hover);
  --bs-dropdown-link-active-color:var(--am-ink);--bs-dropdown-link-active-bg:var(--am-hover);--bs-dropdown-link-disabled-color:#8b90a0;
  --bs-dropdown-item-padding-x:10px;--bs-dropdown-item-padding-y:8px;--bs-dropdown-item-border-radius:8px;
  --bs-dropdown-header-color:var(--am-muted);--bs-dropdown-header-padding-x:10px;--bs-dropdown-header-padding-y:4px;
  box-shadow:0 1px 2px rgba(16,24,40,.04),0 14px 34px -10px rgba(16,24,40,.22)}
.am-menu.show{animation:am-in .14s ease-out}
.am-menu.wide{width:312px;max-width:calc(100vw - 72px)}
@keyframes am-in{from{opacity:0;translate:0 -4px;scale:.98}}
.am-menu .dropdown-divider{margin-inline:-6px;opacity:1}
.am-menu .dropdown-header{padding-top:8px;font-size:.6875rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
.am-item{display:flex;align-items:center;gap:10px;font-weight:500;line-height:1.4}
.am-item>svg{width:16px;height:16px;flex:none;color:var(--am-muted)}
.am-item:focus{outline:none}
.am-item:focus-visible{outline:2px solid var(--am-accent);outline-offset:-2px}
.am-kbd,.am-hint{margin-left:auto;padding:0 0 0 18px;background:none;color:var(--am-muted);font-size:.75rem}
.am-kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:500}
.am-item.danger,.am-item.danger>svg{color:var(--am-danger)}
.am-item.danger:hover,.am-item.danger:focus{background:var(--am-danger-bg);color:var(--am-danger)}
.am-item.disabled>svg{color:inherit}
.am-item.rich{align-items:flex-start;gap:12px;padding:10px;white-space:normal}
.am-tile{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:var(--am-tint);color:var(--am-accent)}
.am-tile svg{width:18px;height:18px}
.am-title{display:block;font-weight:600}
.am-desc{display:block;margin-top:1px;color:var(--am-muted);font-size:.8rem;font-weight:400;line-height:1.4}
@media (prefers-reduced-motion:reduce){.am-menu.show{animation:none}.am-btn,.am-btn svg{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z"/><path d="m13.5 6.5 4 4"/></symbol>
  <symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="12" height="12" rx="2"/><path d="M15.5 8.5V5a1.5 1.5 0 0 0-1.5-1.5H5A1.5 1.5 0 0 0 3.5 5v9A1.5 1.5 0 0 0 5 15.5h3.5"/></symbol>
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>
  <symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></symbol>
  <symbol id="i-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3 10h18M7 15h3"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10.5l5 5 5-5M5 19.5h14"/></symbol>
  <symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V5M7 9.5l5-5 5 5M5 19.5h14"/></symbol>
  <symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8.5V3.5h10v5"/><rect x="3.5" y="8.5" width="17" height="8" rx="2"/><path d="M7 14h10v6.5H7z"/></symbol>
  <symbol id="i-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="16" rx="2.5"/><path d="M9.2 4v16M14.8 4v16"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-form" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4.5" y="3.5" width="15" height="17" rx="2"/><path d="M8.5 8h7M8.5 12h7M8.5 16h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Icons, shortcuts and a danger item</span>
<div class="relative min-h-[430px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5">
<div class="relative inline-block text-[.875rem]">
<button class="group inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#d3d7e0] bg-white pr-3 pl-3.5 text-[.875rem] font-semibold text-[#1d2130] shadow-[0_1px_2px_rgba(16,24,40,.06)] transition-colors duration-150 hover:bg-[#f7f8fb] aria-expanded:border-[#bfc4d1] aria-expanded:bg-[#f1f2f7] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" id="am-a-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-a">Actions<svg class="size-4 flex-none text-[#5c6273] transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="absolute top-[calc(100%+6px)] left-0 z-20 min-w-[236px] origin-top-left rounded-xl border border-[#e6e8ee] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.22)] animate-menu-in motion-reduce:animate-none" role="menu" id="am-a" aria-labelledby="am-a-btn" hidden>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+E"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-pencil"/></svg><span>Edit</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#5c6273]" aria-hidden="true">⌘E</kbd></button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+D"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-copy"/></svg><span>Duplicate</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#5c6273]" aria-hidden="true">⌘D</kbd></button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+S"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-share"/></svg><span>Share…</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#5c6273]" aria-hidden="true">⇧⌘S</kbd></button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-folder"/></svg>Move to folder</button>
<div class="-mx-1.5 my-1.5 h-px bg-[#e6e8ee]" role="separator"></div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-archive"/></svg>Archive</button>
<div class="-mx-1.5 my-1.5 h-px bg-[#e6e8ee]" role="separator"></div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#c42b2b] hover:bg-[#fdf1f1] focus:bg-[#fdf1f1]" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+Backspace"><svg class="size-4 flex-none" aria-hidden="true"><use href="#i-trash"/></svg><span>Delete</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#5c6273]" aria-hidden="true">⌘⌫</kbd></button>
</div>
</div>
</div></div>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Group headers and a disabled item</span>
<div class="relative min-h-[430px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5">
<div class="relative inline-block text-[.875rem]">
<button class="group inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#d3d7e0] bg-white pr-3 pl-3.5 text-[.875rem] font-semibold text-[#1d2130] shadow-[0_1px_2px_rgba(16,24,40,.06)] transition-colors duration-150 hover:bg-[#f7f8fb] aria-expanded:border-[#bfc4d1] aria-expanded:bg-[#f1f2f7] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" id="am-b-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-b">Invoice INV-2041<svg class="size-4 flex-none text-[#5c6273] transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="absolute top-[calc(100%+6px)] left-0 z-20 min-w-[236px] origin-top-left rounded-xl border border-[#e6e8ee] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.22)] animate-menu-in motion-reduce:animate-none" role="menu" id="am-b" aria-labelledby="am-b-btn" hidden>
<div role="group" aria-labelledby="am-b-h1">
<div class="px-2.5 pt-2 pb-1 text-[.6875rem] font-bold tracking-[.07em] text-[#5c6273] uppercase" id="am-b-h1">Send</div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-mail"/></svg>Email to Harbor & Pine</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-link"/></svg>Copy payment link</button>
</div>
<div class="-mx-1.5 my-1.5 h-px bg-[#e6e8ee]" role="separator"></div>
<div role="group" aria-labelledby="am-b-h2">
<div class="px-2.5 pt-2 pb-1 text-[.6875rem] font-bold tracking-[.07em] text-[#5c6273] uppercase" id="am-b-h2">Payments</div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1" aria-disabled="true" aria-labelledby="am-b-t3" aria-describedby="am-b-d3"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-card"/></svg><span id="am-b-t3">Record payment</span><span class="ml-auto pl-[18px] text-[.75rem] text-[#5c6273]" id="am-b-d3">Paid 12 Sep</span></button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-undo"/></svg>Issue refund</button>
</div>
<div class="-mx-1.5 my-1.5 h-px bg-[#e6e8ee]" role="separator"></div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-download"/></svg>Download PDF</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7] aria-disabled:cursor-not-allowed aria-disabled:text-[#8b90a0] aria-disabled:hover:bg-transparent" role="menuitem" tabindex="-1" aria-keyshortcuts="Meta+P"><svg class="size-4 flex-none text-[#5c6273]" aria-hidden="true"><use href="#i-printer"/></svg><span>Print</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#5c6273]" aria-hidden="true">⌘P</kbd></button>
</div>
</div>
</div></div>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Items with descriptions</span>
<div class="relative min-h-[430px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5">
<div class="relative inline-block text-[.875rem]">
<button class="group inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#4f46e5] bg-[#4f46e5] px-3 text-[.875rem] font-semibold text-white shadow-[0_1px_2px_rgba(16,24,40,.1),0_8px_18px_-8px_rgba(79,70,229,.7)] transition-colors duration-150 hover:border-[#4338ca] hover:bg-[#4338ca] aria-expanded:border-[#4338ca] aria-expanded:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" id="am-c-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="am-c"><svg class="size-4 flex-none" aria-hidden="true"><use href="#i-plus"/></svg>New<svg class="size-4 flex-none transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="absolute top-[calc(100%+6px)] left-0 z-20 min-w-[236px] origin-top-left rounded-xl border border-[#e6e8ee] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.22)] animate-menu-in motion-reduce:animate-none w-[312px] max-w-[calc(100vw-72px)]" role="menu" id="am-c" aria-labelledby="am-c-btn" hidden>
<button type="button" class="flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7]" role="menuitem" tabindex="-1" aria-labelledby="am-c-t1" aria-describedby="am-c-d1"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#eef0ff] text-[#4f46e5]"><svg class="size-[18px]" aria-hidden="true"><use href="#i-board"/></svg></span><span><span class="block font-semibold" id="am-c-t1">Project</span><span class="mt-px block text-[.8rem] leading-[1.4] font-normal text-[#5c6273]" id="am-c-d1">A board with tasks, owners and due dates</span></span></button>
<button type="button" class="flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7]" role="menuitem" tabindex="-1" aria-labelledby="am-c-t2" aria-describedby="am-c-d2"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#eef0ff] text-[#4f46e5]"><svg class="size-[18px]" aria-hidden="true"><use href="#i-file"/></svg></span><span><span class="block font-semibold" id="am-c-t2">Document</span><span class="mt-px block text-[.8rem] leading-[1.4] font-normal text-[#5c6273]" id="am-c-d2">Long-form notes the team can comment on</span></span></button>
<button type="button" class="flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7]" role="menuitem" tabindex="-1" aria-labelledby="am-c-t3" aria-describedby="am-c-d3"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#eef0ff] text-[#4f46e5]"><svg class="size-[18px]" aria-hidden="true"><use href="#i-form"/></svg></span><span><span class="block font-semibold" id="am-c-t3">Form</span><span class="mt-px block text-[.8rem] leading-[1.4] font-normal text-[#5c6273]" id="am-c-d3">Collect replies straight into a table</span></span></button>
<div class="-mx-1.5 my-1.5 h-px bg-[#e6e8ee]" role="separator"></div>
<button type="button" class="flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal no-underline focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#1d2130] hover:bg-[#f1f2f7] focus:bg-[#f1f2f7]" role="menuitem" tabindex="-1" aria-labelledby="am-c-t4" aria-describedby="am-c-d4"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#eef0ff] text-[#4f46e5]"><svg class="size-[18px]" aria-hidden="true"><use href="#i-upload"/></svg></span><span><span class="block font-semibold" id="am-c-t4">Import from CSV</span><span class="mt-px block text-[.8rem] leading-[1.4] font-normal text-[#5c6273]" id="am-c-d4">Up to 10,000 rows, mapped column by column</span></span></button>
</div>
</div>
</div></div>
CSS
@theme {
  --animate-menu-in: menu-in .14s ease-out;
  @keyframes menu-in { from { opacity: 0; translate: 0 -4px; scale: .98; } }
}
JavaScript
// Menu button (WAI-ARIA APG pattern). A trigger has aria-haspopup="menu", aria-expanded and aria-controls="<menu id>".
// Click, Enter, Space or ArrowDown opens the menu (ArrowUp opens it on the last item). Inside: ArrowUp/ArrowDown move,
// Home/End jump, typing a letter jumps to the next item that starts with it, Esc closes and returns focus to the trigger,
// Tab or a click outside closes. Choosing an item closes the menu; checkbox items stay open so you can tick several.
(() => {
  const OPEN = '[aria-haspopup="menu"][aria-expanded="true"]';
  const menuOf = t => document.getElementById(t.getAttribute('aria-controls'));
  const ownerOf = m => m && document.querySelector(`[aria-controls="${m.id}"][aria-expanded="true"]`);
  const itemsOf = m => [...m.querySelectorAll('[role^="menuitem"]')]
    .filter(i => i.closest('[role="menu"]') === m && i.getAttribute('aria-disabled') !== 'true');

  function close(t, focus) {
    const m = menuOf(t);
    m.querySelectorAll(OPEN).forEach(s => close(s));
    t.setAttribute('aria-expanded', 'false');
    m.hidden = true;
    if (focus) t.focus();
  }
  function open(t, at) {
    const level = t.closest('[role="menu"]');
    document.querySelectorAll(OPEN).forEach(o => { if (o !== t && o.closest('[role="menu"]') === level) close(o); });
    t.setAttribute('aria-expanded', 'true');
    menuOf(t).hidden = false;
    if (at !== undefined) itemsOf(menuOf(t)).at(at)?.focus();
  }
  const closeAll = target => document.querySelectorAll(OPEN).forEach(t => {
    if (!t.closest('[role="menu"]') && !t.contains(target) && !menuOf(t).contains(target)) close(t);
  });

  document.addEventListener('click', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t) {
      if (!t.closest('[role="menu"]') && t.getAttribute('aria-expanded') === 'true') close(t);
      else open(t, e.detail === 0 ? 0 : undefined); // keyboard clicks (Enter/Space) move focus into the menu
      return;
    }
    const item = e.target.closest('[role^="menuitem"]');
    if (item && item.getAttribute('aria-disabled') !== 'true') {
      const role = item.getAttribute('role');
      if (role === 'menuitemcheckbox') { item.setAttribute('aria-checked', item.getAttribute('aria-checked') !== 'true'); return; }
      if (role === 'menuitemradio') {
        const set = item.closest('[role="group"]') || item.closest('[role="menu"]');
        set.querySelectorAll('[role="menuitemradio"]').forEach(r => r.setAttribute('aria-checked', r === item));
      }
      let t = ownerOf(item.closest('[role="menu"]'));
      while (t && t.closest('[role="menu"]')) t = ownerOf(t.closest('[role="menu"]'));
      if (t) close(t, true);
      return;
    }
    closeAll(e.target);
  });

  document.addEventListener('keydown', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t && !t.closest('[role="menu"]')) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(t, e.key === 'ArrowDown' ? 0 : -1); }
      else if (e.key === 'Escape' && t.getAttribute('aria-expanded') === 'true') close(t);
      return;
    }
    const m = e.target.closest('[role="menu"]');
    if (!m) return;
    const items = itemsOf(m), i = items.indexOf(e.target), owner = ownerOf(m);
    let next;
    switch (e.key) {
      case 'ArrowDown': next = items[(i + 1) % items.length]; break;
      case 'ArrowUp': next = items.at(i <= 0 ? -1 : i - 1); break;
      case 'Home': next = items[0]; break;
      case 'End': next = items.at(-1); break;
      case 'Escape': if (owner) { e.preventDefault(); close(owner, true); } return;
      case 'Tab': closeAll(null); return;
      case ' ': if (e.target.tagName === 'A') { e.preventDefault(); e.target.click(); } return;
      default:
        if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
        next = [...items.slice(i + 1), ...items.slice(0, i + 1)]
          .find(x => x.textContent.trim().toLowerCase().startsWith(e.key.toLowerCase()));
    }
    e.preventDefault();
    next?.focus();
  });
})();

About this set

Three action menus of the kind every app needs, each opened from a menu button. Icons, shortcuts and a danger item is an Actions menu with Edit, Duplicate, Share and Move to folder, keyboard shortcuts on the right, dividers, and a red Delete at the bottom. Group headers and a disabled item is an invoice menu split into labelled Send and Payments groups, where Record payment is disabled and says why (Paid 12 Sep). Items with descriptions is a primary New button whose menu gives each choice an icon tile and a one-line explanation, useful when the options need a sentence to tell apart. Use the first for row or toolbar actions, the second when a menu mixes different kinds of task, and the third for a create menu. The trigger carries aria-haspopup, aria-expanded and aria-controls, and the list is a role=”menu” of menuitems. A short script follows the WAI-ARIA menu button pattern: arrow keys move, Home and End jump, a typed letter jumps to the next matching item, Esc closes and returns focus, and a click outside closes. The Bootstrap version uses Bootstrap’s own Dropdown plugin.

What’s included

  • Icons, right-aligned shortcuts, dividers and a danger item
  • Labelled groups with a disabled item that explains why
  • Rich items with an icon tile, title and description
  • WAI-ARIA menu button keyboard support in plain CSS and Tailwind
  • Shortcuts exposed with aria-keyshortcuts
  • Bootstrap version restyled through --bs-dropdown-* variables

Accessibility

Each trigger has aria-haspopup="menu", aria-expanded and aria-controls, and each menu is labelled by its trigger. Groups use role="group" labelled by their visible header, shortcuts are announced through aria-keyshortcuts while the printed symbols are hidden, and rich items are labelled by their title and described by their description. Focus rings are visible on the trigger and every item, and the open animation switches off under prefers-reduced-motion.

Customise it

Colours live in the --am-* custom properties on .am (ink, muted, line, hover, accent, danger). In Bootstrap change the --bs-dropdown-* and --bs-btn-* variables on .am-menu and .am-btn; in Tailwind edit the hex values in the item and menu classes, and min-w-[236px] or w-[312px] for width.