Why a dropdown menu reopens when you click its button again

No. 1 · · 5 min read

Learn why a menu can refuse to close on a second click, how the order of browser events causes it, and how to fix it cleanly.

Last week we fixed a small, annoying bug in the Hushread web app. Each story card shows a badge with the number of sources covering that story. Click the badge and a menu lists them. Click the badge again and the menu should close, but it opened again instead.

The Share button on each card did the same thing. Nothing crashed and no error appeared. The menu simply would not listen.

Two rules that were each correct

Our menu followed two rules.

The first rule: when you press anywhere outside the menu, the menu closes. This is what people expect. You open a menu, change your mind, click somewhere else, and it goes away.

The second rule: when you click the button and the menu is closed, the menu opens.

Read those rules one at a time and you will find nothing wrong. The bug lives in the space between them. The button sits outside the menu, so a press on the button counts as a press outside the menu. We wrote the first rule and forgot that.

A click is not one moment

Here is the part that surprises many new developers. To you, a click feels like one thing. To the browser, it is a short series of events. An event is a message the browser sends to your code to say that something just happened.

When you click with a mouse, the browser sends these in order:

  1. mousedown: the button on your mouse went down.
  2. mouseup: the button came back up.
  3. click: a full press and release happened on the same thing.

Code can listen for any of these. Now follow the second click, with the menu already open:

  1. You press. The browser sends mousedown. The first rule sees a press outside the menu and closes it.
  2. You let go.
  3. The browser sends click. The second rule sees a closed menu and opens it.

Each rule did its job. You wanted the menu closed, and you ended with it open.

Here is an everyday way to picture it. You walk across a room to turn off the light. A helpful friend sees you coming and turns it off first, at a second switch. You reach your switch and flip it without looking, so the light comes back on. Nobody made a mistake alone. You were both acting on the same wish, one after the other, and neither of you knew about the other.

All of this happens within one quick click. The menu closes and reopens so fast that it can look as if the second click did nothing at all. You might think the button is broken, when the real trouble is two rules taking turns.

Here is a small made-up version, a filter menu in a to-do list app:

// event.target is the thing that was pressed.
// menu.contains(...) asks: is that thing inside the menu?
document.addEventListener("mousedown", (event) => {
  if (!menu.contains(event.target)) menu.hidden = true;
});

// The button toggles the menu: open or closed
button.addEventListener("click", () => {
  menu.hidden = !menu.hidden;
});

Why it was easy to miss

The first click works. Clicking elsewhere works. Only one path fails: closing the menu with the same button that opened it.

If you test a menu by opening it and then clicking away, you never walk that path. Each small check passes, so the bug can sit there until someone uses the app in that one ordinary way.

What we changed

We told the menu which button opened it. Now the first rule has an exception: a press on that button is left alone. The menu stays open through the press. Then the button’s click toggles it, which means it opens a closed menu and closes an open one. The outside rule no longer gets a say when you use the button, so only one piece of code decides.

In the to-do example, the change looks like this:

document.addEventListener("mousedown", (event) => {
  // Was the press inside the menu, or on its button?
  const onMenu = menu.contains(event.target);
  const onButton = button.contains(event.target);
  if (!onMenu && !onButton) menu.hidden = true;
});

There is a tempting shortcut that we did not take. You could start a timer when the menu closes, and make the button ignore clicks until the timer ends. That is a guess about timing, and a click has no fixed length. If someone holds the mouse button down longer than your timer, their click arrives after it ends, and the bug comes back for them.

We changed this for the sources badge and the Share button, and the fix is live. We left the app’s other menus as they were.

The lesson is bigger than menus. When two pieces of code both react to the same action, each one can be right and the result can still be wrong. If something behaves backwards, stop reading the code one piece at a time. Write down each event in order, and next to it write who acts on it and what they change. The problem often becomes plain once it sits on paper.

What you can try

  • Find a dropdown or popup in your own project. Open it, then click the same button that opened it. See whether it closes.
  • Add a console.log line to listeners for mousedown, mouseup and click on one button. Click it slowly and watch the order in your browser’s console. Hold the mouse button down for a few seconds and notice how late click arrives.
  • Pick one bug that confused you recently and write out its timeline: what happened first, what happened next, and which code ran at each step.

Words to know

  • event: a message the browser sends to your code to say something happened, such as a press or a key tap.
  • listener: a piece of code that waits for one kind of event and runs when it arrives.
  • dropdown menu: a list that appears when you click a button and goes away when you are done with it.
  • toggle: to switch between two states, such as open and closed, using the same action.

Join our journey.

One email a week: what we built, what broke, and what we learned.

We’ll send one email to confirm. Unsubscribe with one click, any time. See our privacy note.