Complete CSS Course Module 12: Intermediate: CSS Components and UI Patterns

CSS Dropdown Menus

  • Beginner
  • Lesson 12.3
  • 6 min read
  • 53 of 77 in the course

In short

CSS dropdown menus show a hidden list of links when you hover or focus a button. Learn hover dropdowns, navbar dropdowns, :focus-within, details/summary and popover.

CSS dropdown menus show a hidden list of links when you hover or focus a button. Learn hover dropdowns, navbar dropdowns, :focus-within, details/summary and popover.

How a CSS dropdown works

A dropdown menu is a list that stays hidden until you need it, like a drawer that slides out when you touch the handle. You met the stacking problem of dropdowns in the z-index lesson. Now we build them properly.

Every CSS dropdown has the same three parts:

  1. A wrapper with position: relative. It is the anchor that the menu is placed against.
  2. A trigger, the button or link people point at.
  3. A menu with position: absolute, placed just below the trigger and hidden at first.

When the wrapper is hovered, we show the menu.

<div class="dropdown">
  <button class="dropdown-toggle" type="button">Products ▾</button>
  <ul class="dropdown-menu">
    <li><a href="/laptops">Laptops</a></li>
    <li><a href="/phones">Phones</a></li>
    <li><a href="/tablets">Tablets</a></li>
  </ul>
</div>
.dropdown {
  position: relative;
  display: inline-block;
}

.dropdown-toggle {
  padding: 10px 16px;
  border: 0;
  background: #1f2937;
  color: white;
  font: inherit;
  cursor: pointer;
}

.dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 10;
  min-width: 180px;
  margin: 0;
  padding: 6px 0;
  list-style: none;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
}

.dropdown-menu a {
  display: block;
  padding: 8px 16px;
  color: #1f2937;
  text-decoration: none;
}

.dropdown-menu a:hover {
  background: #f3f4f6;
}

.dropdown:hover .dropdown-menu {
  display: block;
}

Move your mouse over "Products" and a white card with three links appears right under it. top: 100% means "start exactly at the bottom of the wrapper". The z-index keeps the menu above the page content below it. Move the mouse away and the menu disappears again.

Common mistake: Adding margin-top: 10px to the menu to create a gap. The gap is not part of the wrapper, so when the mouse crosses it, hover is lost and the menu closes before you reach it. Use padding-top on the menu instead, or keep the menu touching the trigger.

Make the dropdown work for keyboard users with :focus-within

Hover only works with a mouse. Keyboard users press Tab, and touch screen users tap. The pseudo-class :focus-within matches an element when it or anything inside it has focus. When someone tabs to the toggle button, the wrapper matches :focus-within, and so does it when they tab into the links inside the menu.

.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu {
  display: block;
}

.dropdown-toggle:focus-visible,
.dropdown-menu a:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: -3px;
}

Press Tab until the "Products" button is focused: the menu opens. Press Tab again and the focus moves into Laptops, Phones, Tablets, and the menu stays open. Tab past the last link and it closes. On touch screens it depends on the browser (Safari does not focus a button when you tap it), which is one more reason to look at the click-based options below.

Note: A pure CSS dropdown is fine for simple sites. A full accessible menu also needs aria-expanded on the button, closing with the Escape key and arrow key support. That needs JavaScript. The two no-JS options below handle more of this for you.

A navbar with a dropdown

Most dropdowns live in a navigation bar. Here the dropdown is one item in a flex row.

<nav class="navbar" aria-label="Main">
  <a href="/">Home</a>
  <div class="nav-item">
    <a href="/services">Services ▾</a>
    <ul class="submenu">
      <li><a href="/services/web">Web design</a></li>
      <li><a href="/services/seo">SEO</a></li>
      <li><a href="/services/hosting">Hosting</a></li>
    </ul>
  </div>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>
.navbar {
  display: flex;
  background: #0f172a;
}

.navbar a {
  display: block;
  padding: 14px 18px;
  color: white;
  text-decoration: none;
}

.navbar a:hover {
  background: #1e293b;
}

.nav-item {
  position: relative;
}

.submenu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 10;
  min-width: 200px;
  margin: 0;
  padding: 0;
  list-style: none;
  background: #1e293b;
}

.nav-item:hover .submenu,
.nav-item:focus-within .submenu {
  display: block;
}

You see a dark navbar with Home, Services, About and Contact. Hover or tab to "Services" and a dark submenu appears below it, lined up with its left edge. The "Services" link itself still goes to the services page, so touch users who can't hover still reach every page.

A dropdown with no JavaScript: details and summary

The HTML <details> element is a built-in "open and close" box. The <summary> is the clickable title. It works with a mouse, the keyboard (Enter or Space) and touch, and screen readers announce whether it is open or closed. We only need CSS to make it look like a dropdown.

<details class="menu">
  <summary>Account</summary>
  <ul>
    <li><a href="/profile">Profile</a></li>
    <li><a href="/settings">Settings</a></li>
    <li><a href="/logout">Log out</a></li>
  </ul>
</details>
.menu {
  position: relative;
  display: inline-block;
}

.menu summary {
  list-style: none; /* hides the default triangle */
  padding: 10px 16px;
  background: #2563eb;
  color: white;
  border-radius: 8px;
  cursor: pointer;
}
.menu summary::-webkit-details-marker { display: none; } /* older Safari */

.menu summary::after {
  content: " ▾";
}
.menu[open] summary::after {
  content: " ▴";
}

.menu ul {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 10;
  min-width: 160px;
  margin: 0;
  padding: 6px 0;
  list-style: none;
  background: white;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
}

.menu ul a {
  display: block;
  padding: 8px 16px;
  color: #1f2937;
  text-decoration: none;
}

Click "Account ▾" and the menu opens below it, and the arrow flips to "▴". Click again to close it. The browser adds the open attribute when the box is open, so .menu[open] lets us style the open state. Here a 4px gap is fine, because opening is done by clicking, not hovering.

The newest option is the HTML popover attribute, which you meet in the Advanced module. A <button popovertarget="menu"> opens a <div id="menu" popover>, and the browser puts it on the top layer (above everything, no z-index needed) and closes it when you click outside or press Escape.

MethodOpens onKeyboardCloses on outside click
:hoverMouse hoverNoYes (mouse leaves)
:hover + :focus-withinHover, Tab, tapYesWhen focus leaves
<details> / <summary>Click, Enter, SpaceYesNo (click summary again)
popover attributeClick, Enter, SpaceYesYes, and Escape

Learn more about the built-in disclosure element on MDN: details.

Practice exercise

Build a "Language" dropdown for a website header using <details> and <summary>, with the options English, Español and Français. Push it to the right side of a flex header, and make the menu open aligned to the right edge so it does not go off the screen.

Hint: use margin-left: auto on the details element inside the flex header, and give the menu right: 0 instead of left: 0.

Key takeaways

  • A dropdown is a position: relative wrapper plus a position: absolute menu with top: 100%.
  • Show the menu with :hover, and add :focus-within so keyboard and touch users can open it.
  • Avoid gaps between trigger and menu in hover dropdowns, or the menu closes too early.
  • <details> and <summary> give a click dropdown with no JavaScript; style the open state with [open].
  • The popover attribute is a modern no-JS option that sits on the top layer and closes on Escape.