Focus is where the keyboard is. When your JavaScript changes the page, focus can be lost or left in a confusing place. Managing it is a core part of accessible code.
focus()
<button id="open">Show form</button>
<form id="form" hidden>
<label>Name <input id="name"></label>
</form>
<script>
document.querySelector("#open").addEventListener("click", () => {
const form = document.querySelector("#form");
form.hidden = false;
document.querySelector("#name").focus(); // move the cursor to the first field
});
</script>
Focus the thing the user should interact with next.
When to move focus
| Situation | Where focus should go |
|---|---|
| A dialog or menu opens | into it (first control, or the dialog itself) |
| A dialog closes | back to the button that opened it |
| A form fails validation | the first invalid field |
| Content is deleted | the next item, or the list |
| A “page” changes in a single-page app | the new main heading or content |
| A skip link is used | the main content |
Returning focus
Remember what had focus before and restore it.
<button id="open">Open panel</button>
<div id="panel" hidden tabindex="-1">
<p>Panel content</p>
<button id="close">Close</button>
</div>
<script>
const open = document.querySelector("#open");
const panel = document.querySelector("#panel");
open.addEventListener("click", () => {
panel.hidden = false;
panel.focus(); // tabindex="-1" lets us focus a non-interactive element
});
document.querySelector("#close").addEventListener("click", () => {
panel.hidden = true;
open.focus(); // back where the user came from
});
</script>
tabindex="-1" makes any element focusable from script without adding it to the Tab order.
Focus after removing something
function deleteItem(item) {
const next = item.nextElementSibling ?? item.previousElementSibling ?? list;
item.remove();
next.setAttribute("tabindex", "-1");
next.focus();
}
Otherwise focus falls back to the top of the page, and a keyboard user must tab all the way back.
Skip links
A hidden-until-focused link at the top of the page lets keyboard users jump past the navigation.
<a class="skip-link" href="#main">Skip to main content</a>
...
<main id="main" tabindex="-1">...</main>
Focus traps (for modal dialogs)
While a modal dialog is open, Tab should stay inside it. The modern way is the native <dialog> with showModal(), which handles it for you: Dialog and Modal Basics. If you build one by hand, you must loop focus between the first and last control and make the rest of the page inert.
Visible focus
Users must see where focus is. Keep or restyle the outline: :focus-visible { outline: 3px solid ...; }.
Detecting focus
element.addEventListener("focus", () => { ... });
element.addEventListener("blur", () => { ... });
console.log(document.activeElement); // the focused element
preventScroll
element.focus({ preventScroll: true }) avoids jumping the page when you focus something off-screen. Use it carefully: the user normally needs to see the focused element.