Component Library

← Back to gallery

Dropdown menu

Category: Navigation · Tags: dropdown, menu-button, disclosure, links, keyboard

How to use it

Use to group a handful of related destinations under one label in a toolbar or header, such as account pages. Do not use it for actions that change data (use buttons, or a full ARIA menu pattern) or for choosing a form value (use a <select>). Replace the hrefs and labels; give the list a new id and update aria-controls if you use more than one copy.

Accessibility

Built as a disclosure of links rather than an ARIA menu, because its items navigate: a native <button> with aria-expanded and aria-controls opens a plain list of <a> elements, so every link stays in the normal Tab order and works without JavaScript knowledge of menus. Escape closes the list and returns focus to the button, ArrowDown/ArrowUp move between links, and clicking outside closes it; the list starts hidden and .dcl-dropdown-menu__list[hidden] keeps it hidden. The button is #0f172a on #ffffff (17.85:1) and the links #0f172a on #ffffff (17.85:1), #0f172a on #f1f5f9 while hovered (16.30:1).

Source (copy-ready)

<div class="dcl-dropdown-menu">
  <button type="button" class="dcl-dropdown-menu__button" aria-expanded="false" aria-controls="dcl-dropdown-menu-list">
    <span class="dcl-dropdown-menu__label">Account</span>
    <svg class="dcl-dropdown-menu__chevron" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M4 6l4 4 4-4" stroke="#0f172a" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
  </button>
  <ul class="dcl-dropdown-menu__list" id="dcl-dropdown-menu-list" hidden>
    <li><a class="dcl-dropdown-menu__link" href="../index.html">Profile</a></li>
    <li><a class="dcl-dropdown-menu__link" href="../index.html">Billing</a></li>
    <li><a class="dcl-dropdown-menu__link" href="../about.html">Help centre</a></li>
  </ul>
</div>
<style>
.dcl-dropdown-menu { position: relative; display: inline-block; max-width: 100%; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-dropdown-menu__button { display: inline-flex; align-items: center; gap: 0.375rem; min-height: 2.75rem; padding: 0.5rem 0.875rem; border: 1px solid #94a3b8; border-radius: 0.375rem; font: inherit; cursor: pointer; color: #0f172a; background-color: #ffffff; }
.dcl-dropdown-menu__button:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-dropdown-menu__button:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-dropdown-menu__label { color: #0f172a; background-color: #ffffff; }
.dcl-dropdown-menu__button[aria-expanded="true"] { border-color: #1d4ed8; }
.dcl-dropdown-menu__chevron { flex: none; }
.dcl-dropdown-menu__list { position: absolute; left: 0; top: calc(100% + 0.25rem); z-index: 20; display: block; box-sizing: border-box; min-width: 12rem; max-width: calc(100vw - 2rem); margin: 0; padding: 0.25rem 0; list-style: none; border: 1px solid #cbd5e1; border-radius: 0.375rem; box-shadow: 0 6px 16px #cbd5e1; color: #0f172a; background-color: #ffffff; }
.dcl-dropdown-menu__list[hidden] { display: none; }
.dcl-dropdown-menu__link { display: block; padding: 0.5rem 1rem; text-decoration: none; color: #0f172a; background-color: #ffffff; }
.dcl-dropdown-menu__link:hover { text-decoration: underline; color: #0f172a; background-color: #f1f5f9; }
.dcl-dropdown-menu__link:focus-visible { outline: 2px solid #1d4ed8; outline-offset: -2px; }
</style>
<script>
(function () {
  document.querySelectorAll(".dcl-dropdown-menu").forEach(function (root) {
    if (root.dataset.dclBound) { return; }
    root.dataset.dclBound = "true";
    var button = root.querySelector(".dcl-dropdown-menu__button");
    var list = root.querySelector(".dcl-dropdown-menu__list");
    if (!button || !list) { return; }
    function links() { return Array.prototype.slice.call(list.querySelectorAll("a")); }
    function setOpen(open) {
      button.setAttribute("aria-expanded", open ? "true" : "false");
      list.hidden = !open;
    }
    button.addEventListener("click", function () {
      setOpen(list.hidden);
    });
    button.addEventListener("keydown", function (event) {
      if (event.key === "ArrowDown") {
        event.preventDefault();
        setOpen(true);
        var items = links();
        if (items.length) { items[0].focus(); }
      }
    });
    list.addEventListener("keydown", function (event) {
      var items = links();
      var index = items.indexOf(document.activeElement);
      if (event.key === "ArrowDown" && items.length) {
        event.preventDefault();
        items[(index + 1) % items.length].focus();
      } else if (event.key === "ArrowUp" && items.length) {
        event.preventDefault();
        items[(index - 1 + items.length) % items.length].focus();
      }
    });
    root.addEventListener("keydown", function (event) {
      if (event.key === "Escape" && !list.hidden) {
        setOpen(false);
        button.focus();
      }
    });
    document.addEventListener("click", function (event) {
      if (!list.hidden && !root.contains(event.target)) { setOpen(false); }
    });
  });
})();
</script>

Was this useful? Send feedback.