Component Library

← Back to gallery

Context menu button

Category: Overlays · Tags: menu, more-actions, overflow, menubutton, kebab

Q3 budget review

How to use it

Use to tuck rarely used actions for one row or card behind a single "More actions" button. Do not hide the main action of the row in it, and do not use role="menu" for site navigation links (use a disclosure list of links instead). After copying, change the aria-label to name your item, give the menu and trigger unique ids, and wire each menuitem to its action.

Accessibility

Follows the WAI-ARIA menu button pattern: the native <button> has aria-haspopup="menu", aria-expanded and an aria-label naming the item it acts on, and the list is role="menu" with role="menuitem" buttons. Enter, Space or ArrowDown opens it on the first item (ArrowUp on the last), ArrowUp/ArrowDown/Home/End move between items, Escape closes it and returns focus to the trigger, Tab or an outside click closes it, and the chosen action is announced in a role="status" line. Menu items are #0f172a on #ffffff (17.85:1), #0f172a on #f1f5f9 on hover (16.30:1); the Delete item is #b91c1c on #ffffff (6.47:1).

Source (copy-ready)

<div class="dcl-context-menu-button">
  <div class="dcl-context-menu-button__row">
    <span class="dcl-context-menu-button__name">Q3 budget review</span>
    <div class="dcl-context-menu-button__anchor">
      <button type="button" class="dcl-context-menu-button__trigger" id="dcl-context-menu-button-trigger" aria-haspopup="menu" aria-expanded="false" aria-controls="dcl-context-menu-button-menu" aria-label="More actions for Q3 budget review"><span aria-hidden="true">&#8943;</span></button>
      <ul class="dcl-context-menu-button__menu" id="dcl-context-menu-button-menu" role="menu" aria-labelledby="dcl-context-menu-button-trigger" hidden>
        <li role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-context-menu-button__item">Rename</button></li>
        <li role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-context-menu-button__item">Duplicate</button></li>
        <li role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-context-menu-button__item">Move to folder</button></li>
        <li role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-context-menu-button__item dcl-context-menu-button__item--danger">Delete</button></li>
      </ul>
    </div>
  </div>
  <p class="dcl-context-menu-button__status" role="status"></p>
</div>
<style>
.dcl-context-menu-button { max-width: 24rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-context-menu-button__row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.6rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
.dcl-context-menu-button__name { min-width: 0; overflow-wrap: anywhere; font-weight: 600; color: #0f172a; background-color: #ffffff; }
.dcl-context-menu-button__anchor { position: relative; flex-shrink: 0; }
.dcl-context-menu-button__trigger { width: 2.5rem; height: 2.5rem; font: inherit; font-size: 1.25rem; line-height: 1; border: 1px solid #94a3b8; border-radius: 6px; cursor: pointer; color: #0f172a; background-color: #ffffff; }
.dcl-context-menu-button__trigger:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-context-menu-button__menu { position: absolute; right: 0; top: calc(100% + 0.25rem); z-index: 20; display: block; min-width: 11rem; margin: 0; padding: 0.25rem; list-style: none; border: 1px solid #94a3b8; border-radius: 8px; box-shadow: 0 6px 18px #cbd5e1; background-color: #ffffff; }
.dcl-context-menu-button__menu[hidden] { display: none; }
.dcl-context-menu-button__item { display: block; width: 100%; padding: 0.5rem 0.75rem; border: none; border-radius: 6px; text-align: left; font: inherit; cursor: pointer; color: #0f172a; background-color: #ffffff; }
.dcl-context-menu-button__item:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-context-menu-button__item--danger { color: #b91c1c; background-color: #ffffff; }
.dcl-context-menu-button__trigger:focus-visible, .dcl-context-menu-button__item:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 1px; }
.dcl-context-menu-button__status { min-height: 1.5em; margin: 0.5rem 0 0; color: #334155; background-color: #ffffff; }
</style>
<script>
(function () {
  document.querySelectorAll(".dcl-context-menu-button").forEach(function (root) {
    if (root.dataset.dclBound) { return; }
    root.dataset.dclBound = "true";
    var trigger = root.querySelector(".dcl-context-menu-button__trigger");
    var menu = root.querySelector(".dcl-context-menu-button__menu");
    var status = root.querySelector(".dcl-context-menu-button__status");
    var name = root.querySelector(".dcl-context-menu-button__name");
    if (!trigger || !menu) { return; }
    var items = Array.prototype.slice.call(menu.querySelectorAll("[role=\"menuitem\"]"));
    if (!items.length) { return; }
    function open(index) {
      menu.hidden = false;
      trigger.setAttribute("aria-expanded", "true");
      items[(index + items.length) % items.length].focus();
    }
    function close(returnFocus) {
      if (menu.hidden) { return; }
      menu.hidden = true;
      trigger.setAttribute("aria-expanded", "false");
      if (returnFocus) { trigger.focus(); }
    }
    trigger.addEventListener("click", function () { if (menu.hidden) { open(0); } else { close(true); } });
    trigger.addEventListener("keydown", function (event) {
      if (event.key === "ArrowDown") { event.preventDefault(); open(0); }
      if (event.key === "ArrowUp") { event.preventDefault(); open(items.length - 1); }
    });
    menu.addEventListener("keydown", function (event) {
      var index = items.indexOf(document.activeElement);
      if (event.key === "ArrowDown") { event.preventDefault(); items[(index + 1) % items.length].focus(); }
      else if (event.key === "ArrowUp") { event.preventDefault(); items[(index - 1 + items.length) % items.length].focus(); }
      else if (event.key === "Home") { event.preventDefault(); items[0].focus(); }
      else if (event.key === "End") { event.preventDefault(); items[items.length - 1].focus(); }
      else if (event.key === "Escape") { event.preventDefault(); close(true); }
      else if (event.key === "Tab") { close(false); }
    });
    items.forEach(function (item) {
      item.addEventListener("click", function () {
        if (status) { status.textContent = item.textContent + " chosen for " + (name ? name.textContent : "this item") + "."; }
        close(true);
      });
    });
    document.addEventListener("click", function (event) {
      if (!menu.hidden && !root.contains(event.target)) { close(false); }
    });
  });
})();
</script>

Was this useful? Send feedback.