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.