How to Create a Dropdown Menu in CSS

This recipe builds a click-toggle dropdown menu with a minimal, accessible JavaScript pattern, and shows the CSS-only hover version for comparison.

Two ways to build a dropdown

A dropdown can be built two common ways. A CSS-only version shows the menu on :hover, which needs no script at all but works poorly on touch devices, since there is no hover state to trigger it, and it is awkward for keyboard users. A JavaScript click-toggle version works identically on touch, mouse, and keyboard, and can be closed with Escape or an outside click. This recipe builds the click-toggle version as the main technique, then shows the hover version for context.

The click-toggle dropdown

The toggle is a real button with aria-haspopup and aria-expanded, and the menu is an absolutely positioned list tucked just under it. A click handler flips an open class with classList.toggle; a second listener closes the menu if a click lands outside both the button and the menu; a third listens for the Escape key so the menu can always be dismissed without a mouse.

Example

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: sans-serif; margin: 40px; }
  .dropdown { position: relative; display: inline-block; }
  .dropdown-toggle {
    padding: 10px 16px;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  .dropdown-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 4px;
    background: white;
    border: 1px solid #e5e7eb;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    list-style: none;
    padding: 4px 0;
    min-width: 160px;
    z-index: 20;
  }
  .dropdown-menu.open { display: block; }
  .dropdown-menu li a {
    display: block;
    padding: 8px 16px;
    color: #111827;
    text-decoration: none;
  }
  .dropdown-menu li a:hover { background: #f3f4f6; }
</style>
</head>
<body>
  <div class='dropdown'>
    <button class='dropdown-toggle' id='ddToggle' aria-haspopup='true' aria-expanded='false'>
      Options &#9662;
    </button>
    <ul class='dropdown-menu' id='ddMenu'>
      <li><a href='#'>Edit</a></li>
      <li><a href='#'>Duplicate</a></li>
      <li><a href='#'>Delete</a></li>
    </ul>
  </div>

  <script>
    const toggle = document.getElementById('ddToggle');
    const menu = document.getElementById('ddMenu');

    toggle.addEventListener('click', () => {
      const isOpen = menu.classList.toggle('open');
      toggle.setAttribute('aria-expanded', isOpen);
    });

    document.addEventListener('click', (e) => {
      if (!toggle.contains(e.target) && !menu.contains(e.target)) {
        menu.classList.remove('open');
        toggle.setAttribute('aria-expanded', 'false');
      }
    });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        menu.classList.remove('open');
        toggle.setAttribute('aria-expanded', 'false');
      }
    });
  </script>
</body>
</html>
Note: Closing only on an outside click is a common trap - it leaves keyboard users with no way to dismiss the menu. Always pair it with an Escape key listener as well.

The CSS-only hover version

For contrast, the simplest possible dropdown needs no JavaScript: wrap the button and menu in a position: relative container, hide the menu by default, and reveal it with :hover or :focus-within on the wrapper. :focus-within matters here - it keeps the menu open while a keyboard user has tabbed into it, not just while the mouse happens to be hovering.

Example

<!DOCTYPE html>
<html>
<head>
<style>
  .dropdown-hover { position: relative; display: inline-block; font-family: sans-serif; }
  .dropdown-hover .dropdown-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    background: white;
    border: 1px solid #e5e7eb;
    list-style: none;
    padding: 4px 0;
    min-width: 140px;
  }
  .dropdown-hover:hover .dropdown-menu,
  .dropdown-hover:focus-within .dropdown-menu {
    display: block;
  }
</style>
</head>
<body>
  <div class='dropdown-hover'>
    <button>Options</button>
    <ul class='dropdown-menu'>
      <li><a href='#'>Edit</a></li>
      <li><a href='#'>Delete</a></li>
    </ul>
  </div>
</body>
</html>

Positioning the menu

Either version relies on the same positioning trick: the wrapper needs position: relative so it becomes the reference point, and the menu needs position: absolute with top: 100% to sit directly below it and left: 0 to align its left edge. A z-index on the menu keeps it layered above whatever content happens to sit underneath.

ApproachTriggerTouch-friendly?Closes on Escape or outside click?
Click + classList.toggleClick or tap the buttonYesYes - built with explicit listeners
:hover / :focus-within (CSS only)Mouse hover, or Tab focusPoor - a tap does not hoverNo explicit close, just moving the mouse or focus away

Exercise: Navigation

What CSS technique makes a navbar stick to the top of the viewport after scrolling, without JavaScript?

Frequently Asked Questions

How do I create a dropdown menu with only HTML and CSS?
Wrap the trigger and the menu in a container with position: relative, hide the menu with display: none, and show it on :hover or :focus-within. Position the menu absolutely so it floats over the page instead of shifting the layout.
How do I make a dropdown accessible with a keyboard?
Use :focus-within alongside :hover so tabbing into the menu opens it, and make the trigger a real button. Hover alone locks out keyboard and touch users, since neither fires a hover state reliably.