Understand the idea

Keyboard focus is the current interaction point. It should move when the task requires it, remain visible, and return somewhere useful when a temporary interface closes.

A browser normally moves through interactive elements in document order. Reordering them visually can make that sequence confusing. Prefer semantic controls and a sensible document order before adding scripted focus.

Read the example

An excerpt for a supporting browser, with an existing button#open and dialog#note. The full recipe checks support, supplies a close button, names the dialog and provides fallback content.

JavaScript · EXAMPLE
const opener = document.querySelector('#open');
const note = document.querySelector('#note');
opener.addEventListener('click', () => note.showModal());
note.addEventListener('close', () => opener.focus());

Three distinct concerns

  • Order: can a visitor follow the task through the document?
  • Visibility: can they see which control will respond?
  • Placement: does focus land somewhere meaningful after a change?

A visible focus outline is essential. Styling :focus-visible lets you provide one without guessing whether a person uses a mouse or keyboard. Avoid positive tabindex values: maintaining a separate numeric order becomes fragile as the page changes.

Opening a modal

A native dialog opened with showModal() makes surrounding content inert. Choose initial focus according to its contents. For a brief informational note, its close button may be suitable. Longer structured content may need initial focus on a static heading with tabindex="-1" so the beginning is not skipped. The dialog needs an accessible name, commonly via aria-labelledby.

Returning from a temporary task

Close a native dialog with its API rather than merely hiding it. Escape is an expected exit. This recipe listens for close to return focus to its opener. In a more complex application, check whether that opener is still in the document and available.

Do not move focus just to announce news

A filtered result count usually belongs in a status region, leaving focus in the filter. A validation failure is different: moving to the first invalid field can support recovery. Read about understandable status messages.

Try the complete dialog recipe. Native behaviour is a useful foundation, but test the result with the browsers and assistive technologies your audience uses.

A small mistake, explained

What goes wrong

A dialog closes but focus is left on an invisible control, or every update unexpectedly sends focus to a heading.

How to fix it. Restore focus to the initiating control when it still exists and is usable. If it was removed, choose the next logical place in the workflow. Keep routine updates from taking focus.

Try it yourself

Open the recipe dialog using the keyboard. Use Tab and Shift+Tab, close with Escape, and confirm that the opener receives focus again. Then test the close button.

Further reading

MDN: The dialog element (new tab)

Original explanation and example prepared for HTML code FYI with AI assistance. Test the code in your own context. How these guides are made.