# Search panels that close on Tab and drop keyboard focus

Source: https://easeweb.dev/learn/panel-dismissal
Topics: Dialogs and overlays > Search and dropdown panels can be closed with a keyboard without losing focus (WCAG 2.1.1, 2.4.3, 4.1.2)
The fix: Either script the panel as a disclosure: aria-expanded on its button, Escape and a close button that return focus to it, and close on focus-out only when focus has left the whole component. Or make the panel a native popover, which gives you Escape, the expanded state and focus return without script.
Test with: keyboard, screen reader
References: https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html https://developer.mozilla.org/en-US/docs/Web/API/Popover_API

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop's header has a Search button. Activate it and a panel opens below with a search field, a short list of results and a Close button. Type "lamp" and press Tab to reach the first result: the panel disappears, and focus with it. Open it again and press Escape: nothing happens. The next Tab starts from somewhere the user didn't choose.

## Why it fails

A `blur` handler on the field closes the panel whenever the field loses focus, including when focus moves to a result inside the same panel. The result is hidden while it has focus, so the browser drops focus to the page. The results can't be reached by keyboard (2.1.1), focus jumps out of the sequence the user was following (2.4.3), and the Search button never says whether its panel is open (4.1.2).

## Who is affected

Keyboard users, screen reader users and switch users can type a query but can't pick a result. When the panel closes, they have to find their place again from the top of the page. Mouse users click a result before the field loses focus, so the bug can go unnoticed for a long time.

## What automation and AI miss

The markup is a named button, a labelled field and ordinary links, so a static scan finds nothing to report. The failure only appears when focus moves after the panel is open. Closing on `blur` is a common suggestion for "close the dropdown when the user clicks away", and AI coding tools reproduce it without checking where focus is going.

## Before: a panel that closes whenever the field loses focus

```html
<button type="button" id="search-trigger">Search</button>
<div id="search-panel" hidden>
  <label for="search-input">Search products</label>
  <input id="search-input" type="search">
  <ul>
    <li><a href="/lamps/arc">Arc floor lamp</a></li>
    <li><a href="/lamps/dome">Dome table lamp</a></li>
  </ul>
  <button type="button" id="search-close">Close</button>
</div>
<button type="button">Cart</button>
```

```js
const trigger = document.getElementById('search-trigger');
const panel = document.getElementById('search-panel');
const input = document.getElementById('search-input');
trigger.addEventListener('click', () => {
  panel.hidden = false;
  input.focus();
});
input.addEventListener('blur', () => { panel.hidden = true; });
document.getElementById('search-close').addEventListener('click', () => {
  panel.hidden = true;
});
```

Tab from the field closes the panel before the result can be used, and the close button can't be reached at all. Nothing handles Escape, nothing returns focus, and the button has no expanded state.

## After: a disclosure that closes when asked

```html
<div id="search">
  <button type="button" id="search-trigger" aria-expanded="false" aria-controls="search-panel">Search</button>
  <div id="search-panel" tabindex="-1" hidden>
    <label for="search-input">Search products</label>
    <input id="search-input" type="search">
    <ul>
      <li><a href="/lamps/arc">Arc floor lamp</a></li>
      <li><a href="/lamps/dome">Dome table lamp</a></li>
    </ul>
    <button type="button" id="search-close">Close search</button>
  </div>
</div>
<button type="button">Cart</button>
```

```js
const search = document.getElementById('search');
const trigger = document.getElementById('search-trigger');
const panel = document.getElementById('search-panel');
const input = document.getElementById('search-input');
function openPanel() {
  panel.hidden = false;
  trigger.setAttribute('aria-expanded', 'true');
  input.focus();
}
function closePanel(returnFocus) {
  panel.hidden = true;
  trigger.setAttribute('aria-expanded', 'false');
  if (returnFocus) trigger.focus();
}
trigger.addEventListener('click', () => (panel.hidden ? openPanel() : closePanel(true)));
document.getElementById('search-close').addEventListener('click', () => closePanel(true));
search.addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !panel.hidden) closePanel(true);
});
// Close when focus leaves the whole component, and leave focus where the user put it.
search.addEventListener('focusout', (event) => {
  if (!search.contains(event.relatedTarget)) closePanel(false);
});
```

The panel follows the button in the markup, so Tab goes from the button to the field, the results, the close button and then Cart. Moving focus inside the component keeps the panel open. Escape and Close search both close it and put focus back on Search, which now says whether its panel is open. Tab or Shift + Tab out of the component closes it without moving focus anywhere else. `tabindex="-1"` on the panel lets a click on its blank space keep focus inside, so that click doesn't close it.

## After: a native popover

```html
<button type="button" popovertarget="search-panel">Search</button>
<div id="search-panel" popover>
  <label for="search-input">Search products</label>
  <input id="search-input" type="search" autofocus>
  <ul>
    <li><a href="/lamps/arc">Arc floor lamp</a></li>
    <li><a href="/lamps/dome">Dome table lamp</a></li>
  </ul>
  <button type="button" popovertarget="search-panel" popovertargetaction="hide">Close search</button>
</div>
<button type="button">Cart</button>
```

The `popover` attribute does the work with no script. The browser exposes the button as expanded or collapsed, and puts the popover right after it in the Tab order. `autofocus` moves focus to the field when it opens. Escape, a click outside and Close search each close it, and when focus was inside, the browser returns it to Search. Tab out of the popover moves on to Cart and leaves the popover open, which is allowed.

## Which option to use

Both pass. The scripted disclosure works in every browser, keeps the panel in the page's own layout, and can close when focus leaves the component, but every piece of it is your code to maintain. The popover needs no script and gets Escape, the expanded state and focus return from the browser. It sits in the top layer, so you position it yourself (with CSS anchor positioning or a few lines of script), and it stays open when the user tabs away. Choose the popover when the browsers you support all have it and an open panel left behind is acceptable. Choose the disclosure when the panel must close on focus-out or lives inside a layout the top layer would break.

## Implementation decisions

Closing on focus-out is a design choice, not a requirement. A panel that stays open after the user tabs away is not a keyboard trap, because focus did leave. What fails is closing while focus is still inside, or pulling focus back in after it has left. If you close on focus-out, check `relatedTarget` against the whole component, never just the field.

Return focus to the button only when the user closes the panel on purpose, with Escape or the close button. When they Tab away or click elsewhere, leave focus where they put it. In browsers that clear a `type="search"` field on Escape, you may let a first Escape clear the text and a second close the panel; test both presses.

This is a non-modal panel. Don't add a focus trap, `aria-modal` or an inert page; those belong to the `modal-focus-management` guide. If focus stays in the field while the arrow keys move through suggestions, the component is a combobox and follows that pattern instead, with `aria-activedescendant` and announced results. Name the close control in words, or see `icon-buttons` for an icon-only ×.

## Verify the fix

1. Open the panel from the keyboard and type a query; focus lands in the field.
2. Tab and Shift + Tab through the field, each result and the close button; the panel stays open throughout.
3. In separate runs, close with Escape and with the close button; each time focus returns to the opening button.
4. Leave the panel with Tab past its last control and with Shift + Tab past its first; focus moves on to the neighbouring control and is never pulled back.
5. With a screen reader, confirm the opening button is announced as expanded or collapsed and that the results can be read.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared header or search component changes.

## Limits

This is a starting pattern for a non-modal search panel, not for comboboxes, menus or dialogs. The snippets omit fetching and announcing results. Test the complete journey, including your real results list, with your target assistive technology.
