SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/dropdown-menus

8 CSS Dropdown Menus

2 Pure CSS6 with JSPublished 2026-10-04Free · no sign-up

Eight dropdowns for apps and websites: an action menu button, a navigation dropdown that opens on hover and focus, a mega menu, a no-JavaScript popover menu, an account menu, a split button, a language switcher and a right-click context menu.

01. Accessible Dropdown Menu Button

MenuARIA

A menu button that opens a list of actions, following the ARIA menu button pattern: aria-haspopup, aria-expanded, role="menu" and menuitems. Arrow keys move through items, Escape closes and returns focus, and clicking outside closes it.

View code
<div class="mb">
  <button id="mb-b" aria-haspopup="true" aria-expanded="false" aria-controls="mb-m">Actions</button>
  <ul id="mb-m" role="menu" aria-labelledby="mb-b" hidden>
    <li role="none"><button role="menuitem">Edit</button></li>
    <li role="none"><button role="menuitem">Duplicate</button></li>
    <li role="none"><button role="menuitem">Archive</button></li>
    <li role="none"><button role="menuitem" class="danger">Delete</button></li>
  </ul>
</div>
.mb { position: relative; font-family: Inter, system-ui, sans-serif; }
#mb-b { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; color: #101828; font: 700 14px Inter, sans-serif; cursor: pointer; }
#mb-b::after { content: ""; width: 6px; height: 6px; border: solid currentColor; border-width: 0 1.5px 1.5px 0; transform: rotate(45deg) translateY(-2px); }
#mb-m { position: absolute; top: calc(100% + 6px); left: 0; min-width: 180px; margin: 0; padding: 6px; list-style: none; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; box-shadow: 0 16px 36px -14px rgba(16,24,40,.35); }
#mb-m button { display: block; width: 100%; padding: 9px 10px; border: 0; border-radius: 8px; background: none; color: #344054; font: 500 14px Inter, sans-serif; text-align: left; cursor: pointer; }
#mb-m button:hover, #mb-m button:focus { background: #f2f4f7; outline: none; color: #101828; }
#mb-m .danger { color: #b42318; }
#mb-b:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
const mb = document.getElementById('mb-b'), mm = document.getElementById('mb-m'), items = [...mm.querySelectorAll('[role=menuitem]')];
function set(o, focusFirst) { mm.hidden = !o; mb.setAttribute('aria-expanded', String(o)); if (o && focusFirst) items[0].focus(); }
mb.addEventListener('click', () => set(mm.hidden, true));
mb.addEventListener('keydown', e => { if (e.key === 'ArrowDown') { e.preventDefault(); set(true, true); } });
mm.addEventListener('keydown', e => {
  const i = items.indexOf(document.activeElement);
  if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length].focus(); }
  if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); }
  if (e.key === 'Escape' || e.key === 'Tab') { set(false); if (e.key === 'Escape') mb.focus(); }
});
items.forEach(it => it.addEventListener('click', () => { set(false); mb.focus(); }));
document.addEventListener('click', e => { if (!e.target.closest('.mb')) set(false); });

02. Navigation Dropdown on Hover and Focus

NavigationCSS

A navbar where a link reveals a submenu on hover and also when anything inside it has keyboard focus, using :focus-within. The submenu links are a normal list, so Tab reaches every one, and a short delay stops the menu closing when the pointer slips.

View code
<nav class="hn" aria-label="Main">
  <ul>
    <li><a href="#">Home</a></li>
    <li class="has"><a href="#" aria-haspopup="true">Shop</a>
      <ul><li><a href="#">New in</a></li><li><a href="#">Kitchen</a></li><li><a href="#">Garden</a></li><li><a href="#">Sale</a></li></ul>
    </li>
    <li><a href="#">Journal</a></li>
  </ul>
</nav>
.hn { font-family: Inter, system-ui, sans-serif; }
.hn > ul { display: flex; gap: 4px; margin: 0; padding: 6px; list-style: none; border-radius: 12px; background: #151a26; }
.hn a { display: block; padding: 9px 14px; border-radius: 8px; color: #e7ebf5; font-weight: 600; font-size: 14px; text-decoration: none; }
.hn a:hover, .hn a:focus-visible { background: #1e2433; outline: none; }
.has { position: relative; }
.has > a::after { content: " \25BE"; color: #9aa4b8; }
.has ul { position: absolute; top: calc(100% + 8px); left: 0; min-width: 170px; margin: 0; padding: 6px; list-style: none; border: 1px solid #2c3346; border-radius: 12px; background: #12161f; opacity: 0; visibility: hidden; transform: translateY(-4px); transition: opacity .15s .1s, visibility .15s .1s, transform .15s .1s; }
.has ul::before { content: ""; position: absolute; left: 0; right: 0; top: -10px; height: 10px; }
.has:hover ul, .has:focus-within ul { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
.has ul a { font-weight: 500; }

03. Mega Menu Panel

NavigationLayout

A wide dropdown with several columns of links and a promotional card, as used by large stores. The panel is toggled by a disclosure button with aria-expanded, which is simpler and more predictable than menu roles for site navigation.

View code
<nav class="mg" aria-label="Main">
  <button aria-expanded="true" aria-controls="mg-p">Products</button>
  <div id="mg-p" class="panel">
    <div><h3>Kitchen</h3><ul><li><a href="#">Mugs</a></li><li><a href="#">Bowls</a></li><li><a href="#">Plates</a></li></ul></div>
    <div><h3>Living</h3><ul><li><a href="#">Vases</a></li><li><a href="#">Throws</a></li><li><a href="#">Candles</a></li></ul></div>
    <a class="promo" href="#"><b>Autumn edit</b><span>New colours in stoneware</span></a>
  </div>
</nav>
.mg { position: relative; width: min(560px, 94vw); font-family: Inter, system-ui, sans-serif; }
.mg > button { padding: 10px 16px; border: 0; border-radius: 10px; background: #101828; color: #fff; font: 700 14px Inter, sans-serif; cursor: pointer; }
.mg > button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.panel { display: grid; grid-template-columns: 1fr 1fr 1.2fr; gap: 20px; margin-top: 10px; padding: 20px; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; box-shadow: 0 20px 40px -20px rgba(16,24,40,.35); }
.mg[data-closed] .panel { display: none; }
.panel h3 { margin: 0 0 8px; color: #5d6679; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
.panel ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.panel a { color: #101828; font-size: 14px; text-decoration: none; }
.panel ul a { display: block; padding: 5px 0; }
.panel a:hover { color: #1d4ed8; }
.panel a:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.promo { display: grid; align-content: end; gap: 2px; padding: 14px; border-radius: 12px; background: linear-gradient(160deg, #fde68a, #f59e0b); }
.promo span { color: #451a03; font-size: 13px; }
@media (max-width: 520px) { .panel { grid-template-columns: 1fr 1fr; } .promo { grid-column: 1 / -1; } }
const mgb = document.querySelector('.mg > button'), mg = document.querySelector('.mg');
mgb.addEventListener('click', () => { const open = mgb.getAttribute('aria-expanded') !== 'true'; mgb.setAttribute('aria-expanded', String(open)); mg.toggleAttribute('data-closed', !open); });

04. Dropdown with the Popover API

PopoverModern

A dropdown that uses the popover attribute and CSS anchor positioning: no JavaScript, light dismiss on outside click, Escape to close and placement directly under its button. Without anchor support it still opens, just in the browser's default position.

View code
<div class="pd">
  <button popovertarget="pd-m" class="pd-b">Share</button>
  <div id="pd-m" popover class="pd-m">
    <a href="#">Copy link</a><a href="#">Email</a><a href="#">Embed code</a>
  </div>
</div>
.pd { font-family: Inter, system-ui, sans-serif; }
.pd-b { anchor-name: --pd; padding: 10px 16px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; color: #fff; font: 700 14px Inter, sans-serif; cursor: pointer; }
.pd-b:focus-visible { outline: 2px solid #22d3ee; outline-offset: 2px; }
.pd-m { position-anchor: --pd; top: anchor(bottom); left: anchor(left); margin: 6px 0 0; min-width: 170px; padding: 6px; border: 1px solid #2c3346; border-radius: 12px; background: #12161f; }
.pd-m a { display: block; padding: 9px 10px; border-radius: 8px; color: #e7ebf5; font-size: 14px; text-decoration: none; }
.pd-m a:hover, .pd-m a:focus-visible { background: #1e2433; outline: none; }

05. User Account Dropdown

AccountAvatar

The account menu found in app headers: an avatar button opens a panel with the person's name and email, a few links and a sign-out button separated by a divider. The avatar button's accessible name includes the person's name.

View code
<div class="ua">
  <button class="av" aria-expanded="false" aria-controls="ua-p" aria-label="Account menu for Sara Khan">SK</button>
  <div id="ua-p" class="ua-p" hidden>
    <p class="who"><b>Sara Khan</b><span>sara@clayworks.shop</span></p>
    <a href="#">Profile</a><a href="#">Billing</a><a href="#">Team settings</a>
    <hr>
    <button class="out">Sign out</button>
  </div>
</div>
.ua { position: relative; display: flex; justify-content: flex-end; width: 260px; font-family: Inter, system-ui, sans-serif; }
.av { width: 40px; height: 40px; border: 2px solid #fff; border-radius: 50%; background: linear-gradient(135deg, #ec4899, #8b5cf6); color: #fff; font: 800 14px Inter, sans-serif; box-shadow: 0 0 0 1px #d0d5dd; cursor: pointer; }
.av:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; }
.ua-p { position: absolute; top: 48px; right: 0; width: 230px; padding: 8px; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; box-shadow: 0 18px 36px -16px rgba(16,24,40,.35); }
.who { display: grid; margin: 4px 8px 8px; font-size: 13px; color: #5d6679; }
.who b { color: #101828; font-size: 14px; }
.ua-p a, .out { display: block; width: 100%; padding: 8px; border: 0; border-radius: 8px; background: none; color: #344054; font: 500 14px Inter, sans-serif; text-align: left; text-decoration: none; cursor: pointer; }
.ua-p a:hover, .out:hover, .ua-p a:focus-visible, .out:focus-visible { background: #f2f4f7; outline: none; }
.ua-p hr { margin: 6px 0; border: 0; border-top: 1px solid #eef0f4; }
.out { color: #b42318; }
const av = document.querySelector('.av'), up = document.getElementById('ua-p');
function set(o) { up.hidden = !o; av.setAttribute('aria-expanded', String(o)); }
av.addEventListener('click', () => set(up.hidden));
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !up.hidden) { set(false); av.focus(); } });
document.addEventListener('click', e => { if (!e.target.closest('.ua')) set(false); });
set(true);

06. Split Button Dropdown

ButtonActions

A primary action with an attached arrow that opens related options, like Publish with Schedule and Save as draft. The two halves are separate buttons with their own names, so it is clear what each one does when reached with Tab.

View code
<div class="spb">
  <div class="grp"><button class="main">Publish</button><button class="arr" aria-label="More publish options" aria-expanded="false" aria-controls="spb-m"></button></div>
  <ul id="spb-m" hidden><li><button>Schedule for later</button></li><li><button>Save as draft</button></li><li><button>Preview</button></li></ul>
</div>
.spb { position: relative; font-family: Inter, system-ui, sans-serif; }
.grp { display: inline-flex; }
.grp button { border: 0; background: #34d399; color: #062b1f; font: 700 14px Inter, sans-serif; cursor: pointer; }
.main { padding: 10px 16px; border-radius: 10px 0 0 10px; }
.arr { width: 38px; border-left: 1px solid rgba(6,43,31,.25) !important; border-radius: 0 10px 10px 0; }
.arr::after { content: ""; display: inline-block; width: 6px; height: 6px; border: solid #062b1f; border-width: 0 2px 2px 0; transform: rotate(45deg) translateY(-2px); }
.grp button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; position: relative; }
#spb-m { position: absolute; top: calc(100% + 6px); left: 0; min-width: 200px; margin: 0; padding: 6px; list-style: none; border: 1px solid #2c3346; border-radius: 12px; background: #12161f; }
#spb-m button { width: 100%; padding: 9px 10px; border: 0; border-radius: 8px; background: none; color: #e7ebf5; font: 500 14px Inter, sans-serif; text-align: left; cursor: pointer; }
#spb-m button:hover, #spb-m button:focus-visible { background: #1e2433; outline: none; }
const arr = document.querySelector('.arr'), sm = document.getElementById('spb-m');
function set(o) { sm.hidden = !o; arr.setAttribute('aria-expanded', String(o)); }
arr.addEventListener('click', () => { set(sm.hidden); if (!sm.hidden) sm.querySelector('button').focus(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !sm.hidden) { set(false); arr.focus(); } });
document.addEventListener('click', e => { if (!e.target.closest('.spb')) set(false); });

07. Language Switcher Dropdown

i18nLinks

A language switcher where each option is a link to the translated page, marked with lang and hreflang attributes so screen readers pronounce each language name correctly. The current language is shown on the button and marked with aria-current.

View code
<div class="lang">
  <button aria-expanded="false" aria-controls="lang-m"><span aria-hidden="true">&#127760;</span> English</button>
  <ul id="lang-m" hidden>
    <li><a href="#" lang="en" hreflang="en" aria-current="true">English</a></li>
    <li><a href="#" lang="ur" hreflang="ur">&#1575;&#1585;&#1583;&#1608;</a></li>
    <li><a href="#" lang="es" hreflang="es">Espa&ntilde;ol</a></li>
    <li><a href="#" lang="de" hreflang="de">Deutsch</a></li>
  </ul>
</div>
.lang { position: relative; font-family: Inter, system-ui, sans-serif; }
.lang button { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 1.5px solid #d0d5dd; border-radius: 999px; background: #fff; color: #101828; font: 600 14px Inter, sans-serif; cursor: pointer; }
.lang button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.lang ul { position: absolute; top: calc(100% + 6px); left: 0; min-width: 160px; margin: 0; padding: 6px; list-style: none; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; box-shadow: 0 16px 32px -14px rgba(16,24,40,.35); }
.lang a { display: block; padding: 8px 10px; border-radius: 8px; color: #344054; font-size: 14px; text-decoration: none; }
.lang a:hover, .lang a:focus-visible { background: #f2f4f7; outline: none; }
.lang a[aria-current] { color: #1d4ed8; font-weight: 700; }
const lb = document.querySelector('.lang button'), lm = document.getElementById('lang-m');
function set(o) { lm.hidden = !o; lb.setAttribute('aria-expanded', String(o)); }
lb.addEventListener('click', () => set(lm.hidden));
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !lm.hidden) { set(false); lb.focus(); } });
set(true);

08. Right-Click Context Menu

MenuJavaScript

A custom context menu that opens where you right-click inside a file area, and also from the keyboard with Shift+F10 or the context menu key. It stays inside the visible area, and Escape or a click elsewhere closes it.

View code
<div class="area" tabindex="0" aria-label="File: report.pdf. Right-click or press Shift+F10 for actions">report.pdf</div>
<ul class="ctx" role="menu" aria-label="File actions" hidden>
  <li role="none"><button role="menuitem">Open</button></li>
  <li role="none"><button role="menuitem">Rename</button></li>
  <li role="none"><button role="menuitem">Download</button></li>
</ul>
.area { display: grid; place-items: center; width: 280px; height: 160px; border: 2px dashed #343c51; border-radius: 16px; color: #c9d1e2; font: 600 15px Inter, system-ui, sans-serif; outline: none; }
.area:focus-visible { border-color: #fbbf24; }
.ctx { position: fixed; z-index: 5; min-width: 160px; margin: 0; padding: 6px; list-style: none; border: 1px solid #2c3346; border-radius: 10px; background: #12161f; box-shadow: 0 16px 30px -12px #000; }
.ctx button { width: 100%; padding: 8px 10px; border: 0; border-radius: 6px; background: none; color: #e7ebf5; font: 500 14px Inter, sans-serif; text-align: left; cursor: pointer; }
.ctx button:hover, .ctx button:focus { background: #1e2433; outline: none; }
const area = document.querySelector('.area'), ctx = document.querySelector('.ctx');
function openAt(x, y) {
  ctx.hidden = false;
  const r = ctx.getBoundingClientRect();
  ctx.style.left = Math.min(x, innerWidth - r.width - 8) + 'px';
  ctx.style.top = Math.min(y, innerHeight - r.height - 8) + 'px';
  ctx.querySelector('button').focus();
}
area.addEventListener('contextmenu', e => { e.preventDefault(); openAt(e.clientX, e.clientY); });
area.addEventListener('keydown', e => { if ((e.shiftKey && e.key === 'F10') || e.key === 'ContextMenu') { e.preventDefault(); const r = area.getBoundingClientRect(); openAt(r.left + 20, r.top + 20); } });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !ctx.hidden) { ctx.hidden = true; area.focus(); } });
document.addEventListener('click', e => { if (!e.target.closest('.ctx')) ctx.hidden = true; });

About these CSS Dropdown Menus

Navigation or menu?

For site navigation, a simple disclosure button with aria-expanded that shows a list of links is easiest to use. The ARIA role="menu" pattern is meant for application actions like Edit and Delete, and it brings stricter keyboard expectations.

Keyboard and Escape

Every dropdown here opens from the keyboard, closes on Escape and returns focus to its trigger. Menus with role=menu also move focus between items with the arrow keys.

Hover menus need focus too

A dropdown that opens on :hover must also open on :focus-within, or keyboard users cannot reach its links. A short transition delay stops it closing when the pointer briefly leaves.

The popover attribute

The popover attribute with popovertarget gives light dismiss and Escape handling for free. Combined with CSS anchor positioning it places the menu under its button without JavaScript.

Frequently asked questions

Which dropdowns work without JavaScript?

The hover nav dropdown and the popover menu.

Should a mega menu open on hover?

Opening on click is more predictable, especially on touch screens. The mega demo uses a disclosure button.

How do I keep a menu on screen?

Measure the menu and clamp its position to the viewport, as the context menu demo does.