Live: try it with the mouse and the keyboard (Tab, arrows, Escape).

Dropdown menu UI component for websites and web apps, shown in Fernway, a fictional productivity product. Shows a menu button on a file row that opens an actions menu with icons, a disabled item and a separator, plus every state (default, hover, focus-visible, open, disabled). Brutalist style: high contrast, Space Grotesk, uppercase headings, thick black borders and hard offset shadows. Comes with copyable HTML, CSS and a few lines of plain JavaScript (keyboard support and ARIA per the WAI-ARIA Authoring Practices), a live demo, and a DESIGN.md with light and dark colour tokens.
#fbfbfb · surface #ffffff · text #15181e · muted #5f636c · primary #7d6800 · accent #4565c7#111316 · surface #22252a · text #ececec · muted #aeb1b8 · primary #c9aa24 · accent #afc6ffPaste the HTML where the component goes, the CSS into your stylesheet and the script before </body>. The tokens block holds the light and dark colours; swap in your own.
<div class="mgd-file">
<svg class="i" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4M9 12h6M9 16h6"/></svg><div class="mgd-file__name"><b>launch-brief.pdf</b><span>2 MB · edited by Priya Nair</span></div>
<div class="mgd-menu">
<button class="mgd-btn" type="button" id="file-menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="file-menu">Actions <svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 9l7 7 7-7"/></svg></button>
<ul class="mgd-menu__list" id="file-menu" role="menu" aria-labelledby="file-menu-btn" hidden>
<li class="mgd-menu__item" role="menuitem" tabindex="-1"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16z"/></svg>Rename</li>
<li class="mgd-menu__item" role="menuitem" tabindex="-1"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4M9 12h6M9 16h6"/></svg>Duplicate</li>
<li class="mgd-menu__item" role="menuitem" tabindex="-1"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>Move to…</li>
<li class="mgd-menu__item" role="menuitem" tabindex="-1"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Copy link</li>
<li class="mgd-menu__item" role="menuitem" tabindex="-1" aria-disabled="true"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>Change owner</li>
<li class="mgd-menu__sep" role="separator"></li>
<li class="mgd-menu__item" role="menuitem" tabindex="-1"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>Delete</li>
</ul>
</div>
</div>
<p class="mgd-menu-out" aria-live="polite"></p>/* Tokens (light + dark) */
:root{--bg:#fbfbfb;--surface:#ffffff;--border:#d7dae1;--text:#15181e;--muted:#5f636c;--primary:#7d6800;--on-primary:#ffffff;--accent:#4565c7;--font-h:"Space Grotesk",Georgia,serif;--font-b:"Space Grotesk",system-ui,sans-serif;--hw:700;
--c-bw:2px;--c-line:var(--text);--c-r:0;--c-pill:0;--c-sh:3px 3px 0 var(--text);--c-pop:6px 6px 0 var(--text);--c-tt:uppercase;--c-ls:.02em;--c-fw:700;--c-fs:13px;--c-ring:3px solid var(--text);--c-ind:4px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#111316;--surface:#22252a;--border:#393b41;--text:#ececec;--muted:#aeb1b8;--primary:#c9aa24;--on-primary:#111111;--accent:#afc6ff;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#111316;--surface:#22252a;--border:#393b41;--text:#ececec;--muted:#aeb1b8;--primary:#c9aa24;--on-primary:#111111;--accent:#afc6ff;color-scheme:dark}
/* Component */
.mgd-sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.mgd-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 16px;border:var(--c-bw) solid var(--c-line);border-radius:var(--c-pill);background:var(--surface);color:var(--text);font:var(--c-fw) var(--c-fs)/1 var(--font-b);text-transform:var(--c-tt);letter-spacing:var(--c-ls);box-shadow:var(--c-sh);cursor:pointer;white-space:nowrap;transition:background-color .12s,border-color .12s}
.mgd-btn:hover,.mgd-btn.is-hover{background:color-mix(in oklab,var(--primary) 9%,var(--surface))}
.mgd-btn:focus-visible,.mgd-btn.is-focus{outline:var(--c-ring);outline-offset:2px}
.mgd-btn[aria-expanded="true"],.mgd-btn.is-open{background:color-mix(in oklab,var(--primary) 15%,var(--surface))}
.mgd-btn--primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.mgd-btn--primary:hover,.mgd-btn--primary.is-hover{background:color-mix(in oklab,var(--primary) 84%,var(--text))}
.mgd-btn:disabled,.mgd-btn[aria-disabled="true"],.mgd-btn.is-disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.mgd-btn svg{flex:none}
.mgd-menu{position:relative;display:inline-block}
.mgd-menu__list{position:absolute;z-index:10;top:calc(100% + 6px);right:0;min-width:220px;margin:0;padding:6px;list-style:none;background:var(--surface);border:var(--c-bw) solid var(--c-line);border-radius:var(--c-r);box-shadow:var(--c-pop)}
.mgd-menu__list[hidden]{display:none}
.mgd-menu__item{display:flex;align-items:center;gap:10px;min-height:38px;padding:0 10px;border-radius:calc(var(--c-r) * .6);color:var(--text);font:500 14px var(--font-b);text-transform:var(--c-tt);letter-spacing:var(--c-ls);cursor:pointer;outline:none}
.mgd-menu__item svg{color:var(--muted)}
.mgd-menu__item:hover,.mgd-menu__item.is-hover{background:color-mix(in oklab,var(--primary) 9%,var(--surface))}
.mgd-menu__item:focus-visible,.mgd-menu__item:focus,.mgd-menu__item.is-focus{background:var(--text);color:var(--bg);box-shadow:inset 0 0 0 2px var(--text)}
.mgd-menu__item[aria-disabled="true"],.mgd-menu__item.is-off{opacity:.45;cursor:not-allowed;background:none;box-shadow:none}
.mgd-menu__item kbd{margin-left:auto;font:12px var(--font-b);color:var(--muted)}
.mgd-menu__sep{height:1px;margin:6px 4px;background:var(--border)}
.mgd-file{display:flex;align-items:center;gap:14px;max-width:640px;padding:14px 16px;background:var(--surface);border:var(--c-bw) solid var(--c-line);border-radius:var(--c-r);box-shadow:var(--c-sh)}
.mgd-file__name{flex:1;min-width:0}
.mgd-file__name b{display:block;font-weight:600}
.mgd-file__name span{color:var(--muted);font-size:13px}
.mgd-menu-out{margin:14px 0 0;color:var(--muted);font-size:14px;min-height:1.5em}
@media (prefers-reduced-motion:reduce){.mgd-menu,.mgd-menu *{transition:none!important;animation-duration:2.4s!important}}document.querySelectorAll(".mgd-menu").forEach(function (wrap) {
var btn = wrap.querySelector("[aria-haspopup]"), menu = document.getElementById(btn.getAttribute("aria-controls")), out = document.querySelector(".mgd-menu-out");
var items = [].slice.call(menu.querySelectorAll("[role=menuitem]")), usable = items.filter(function (i) { return i.getAttribute("aria-disabled") !== "true"; });
function open(at) { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); (at === "last" ? usable[usable.length - 1] : usable[0]).focus(); }
function close(back) { if (menu.hidden) return; menu.hidden = true; btn.setAttribute("aria-expanded", "false"); if (back) btn.focus(); }
function choose(item) { if (item.getAttribute("aria-disabled") === "true") return; close(true); if (out) out.textContent = item.textContent.trim() + " chosen."; }
btn.addEventListener("click", function () { menu.hidden ? open() : close(true); });
btn.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); }
else if (e.key === "ArrowUp") { e.preventDefault(); open("last"); }
});
menu.addEventListener("click", function (e) { var it = e.target.closest("[role=menuitem]"); if (it) choose(it); });
menu.addEventListener("keydown", function (e) {
var i = usable.indexOf(document.activeElement), k = e.key, next = null;
if (k === "ArrowDown") next = usable[(i + 1) % usable.length];
else if (k === "ArrowUp") next = usable[(i - 1 + usable.length) % usable.length];
else if (k === "Home") next = usable[0];
else if (k === "End") next = usable[usable.length - 1];
else if (k === "Escape") { e.preventDefault(); close(true); return; }
else if (k === "Tab") { close(false); return; }
else if (k === "Enter" || k === " ") { e.preventDefault(); choose(document.activeElement); return; }
else if (k.length === 1 && /\S/.test(k)) {
var from = usable.slice(i + 1).concat(usable.slice(0, i + 1));
next = from.filter(function (it) { return it.textContent.trim().toLowerCase().indexOf(k.toLowerCase()) === 0; })[0] || null;
}
if (next) { e.preventDefault(); next.focus(); }
});
document.addEventListener("click", function (e) { if (!wrap.contains(e.target)) close(false); });
});## Component: Dropdown menu
An actions menu behind a button: arrow keys move through the items, Escape closes, and a disabled item is skipped.
- **Pattern:** WAI-ARIA Authoring Practices, Menu button (https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)
- **Roles and attributes:** A button with aria-haspopup="menu", aria-expanded and aria-controls opens a list with role="menu"; items have role="menuitem" and tabindex="-1" (focus is moved by script); a disabled item has aria-disabled="true"; a separator has role="separator".
- **States drawn:** default, hover, focus-visible, open, disabled
- **Focus:** a visible `:focus-visible` outline (3px solid text), never removed
- **Style knobs (brutalist):** border 2px, corners 0, shadow `3px 3px 0 var(--text)`, labels uppercase, tracked .02em
- **Motion:** transitions are short (≤160ms) and switched off under `prefers-reduced-motion: reduce`
- **Code:** HTML + CSS + a few lines of plain JavaScript (no libraries); colours come only from the tokens above, so light and dark both work
### Keyboard
- **Enter / Space / Down arrow:** open and focus the first item
- **Up arrow:** open and focus the last item
- **Up / Down arrow:** move between items (wraps, skips disabled)
- **Home / End:** first / last item
- **A letter:** jump to the next item starting with it
- **Escape:** close and return focus to the button
### Licence
Original Made Good Designs code: free to use and adapt in any project, including commercial ones, with no credit needed. Fonts are Google Fonts under their own licences.Add the Made Good Designs library to your assistant once. Then ask for UI references, brand palettes, font pairings and design tokens in any chat.
https://madegooddesigns.com/inspiration/mcp?ref=ui-dropdown-menu-uiSettings → Connectors → Add custom connector → paste the URL.
Settings → Apps & Connectors → Advanced → Developer mode → Create → paste the URL.
One click, then confirm in Cursor.
One click, then confirm in VS Code (Copilot agent mode).
claude mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-dropdown-menu-ui
gemini mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-dropdown-menu-ui
Add this to the app's MCP config (Windsurf: mcp_config.json).
Fonts: Space Grotesk (Google Fonts)
Prompt copied: paste it into the chat that just opened.
Writing about this component?
search_ui_screens (e.g. “billing settings, editorial style”), get_ui_flow for a whole journey, and get_ui_screen_code to pull the HTML and DESIGN.md straight into your project.