How to Manage Focus with focus and blur Events in JavaScript
Focus is a fundamental part of web interaction. When a user clicks on an input field, tabs to a button, or navigates a page with a keyboard, the browser tracks which element is currently "active" and ready to receive input. This focus state determines where keyboard input goes, which element shows visual indicators like outlines, and how assistive technologies present the page to users.
JavaScript gives you full control over focus through events (focus, blur, focusin, focusout), methods (elem.focus(), elem.blur()), properties (document.activeElement), and HTML attributes (autofocus, tabindex). Understanding these tools lets you build accessible forms with validation on blur, keyboard-navigable custom components, focus traps for modals, and intelligent focus management for single-page applications.
This guide covers every aspect of focus handling, from the basic events through advanced delegation patterns.
focus and blur Events
The focus event fires when an element receives focus (becomes the active element). The blur event fires when an element loses focus.
Basic Usage
<input type="text" id="email" placeholder="Enter your email">
<p id="message"></p>
const input = document.getElementById("email");
const message = document.getElementById("message");
input.addEventListener("focus", () => {
message.textContent = "Please enter a valid email address";
message.style.color = "blue";
input.style.borderColor = "blue";
});
input.addEventListener("blur", () => {
if (input.value && !input.value.includes("@")) {
message.textContent = "Invalid email format";
message.style.color = "red";
input.style.borderColor = "red";
} else {
message.textContent = "";
input.style.borderColor = "";
}
});
When the user clicks on or tabs to the input, the focus handler runs and shows a hint. When the user clicks away or tabs out, the blur handler validates the input.
When focus and blur Fire
const input = document.getElementById("username");
input.addEventListener("focus", (event) => {
console.log("Focused!");
console.log("Related target (what lost focus):", event.relatedTarget);
});
input.addEventListener("blur", (event) => {
console.log("Blurred!");
console.log("Related target (what gained focus):", event.relatedTarget);
});
The relatedTarget property tells you the other element involved in the focus change:
- On
focus:relatedTargetis the element that lost focus (ornullif focus came from outside the page) - On
blur:relatedTargetis the element that gained focus (ornullif focus left the page)
input.addEventListener("blur", (event) => {
if (event.relatedTarget === null) {
console.log("User clicked outside the page or switched tabs");
} else {
console.log("Focus moved to:", event.relatedTarget.tagName);
}
});
Validation on Blur
The most common use of blur is validating form fields when the user finishes entering data:
const form = document.getElementById("registration");
function validateField(input) {
const errorEl = input.nextElementSibling; // Assumes error <span> after each input
let errorMsg = "";
if (input.required && !input.value.trim()) {
errorMsg = "This field is required";
} else if (input.type === "email" && input.value && !input.value.includes("@")) {
errorMsg = "Please enter a valid email";
} else if (input.minLength > 0 && input.value.length < input.minLength) {
errorMsg = `Must be at least ${input.minLength} characters`;
}
if (errorEl && errorEl.classList.contains("error")) {
errorEl.textContent = errorMsg;
}
input.classList.toggle("invalid", !!errorMsg);
return !errorMsg;
}
// Validate each field on blur
form.querySelectorAll("input").forEach(input => {
input.addEventListener("blur", () => validateField(input));
});
The Critical Difference: focus/blur Do NOT Bubble
This is the most important thing to know about these events. Unlike most DOM events, focus and blur do not bubble up through the DOM tree:
const parent = document.getElementById("form-container");
// This handler NEVER fires when child inputs get focus
parent.addEventListener("focus", () => {
console.log("Parent received focus event"); // Never runs!
});
// This handler NEVER fires when child inputs lose focus
parent.addEventListener("blur", () => {
console.log("Parent received blur event"); // Never runs!
});
Because they do not bubble, you cannot use event delegation with focus and blur. This is why the bubbling variants focusin and focusout exist.
focusin and focusout (Bubbling Variants)
focusin and focusout are identical to focus and blur in meaning, but they bubble through the DOM tree. This makes them usable with event delegation.
Basic Comparison
const container = document.getElementById("form-container");
// focus: does NOT bubble: handler on parent never fires for child focus
container.addEventListener("focus", () => {
console.log("focus event on container"); // Never fires for children
});
// focusin: DOES bubble: handler on parent fires for child focus
container.addEventListener("focusin", () => {
console.log("focusin event on container"); // Fires when any child gets focus
});
// blur: does NOT bubble
container.addEventListener("blur", () => {
console.log("blur event on container"); // Never fires for children
});
// focusout: DOES bubble
container.addEventListener("focusout", () => {
console.log("focusout event on container"); // Fires when any child loses focus
});
Event Delegation with focusin/focusout
<form id="user-form">
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name">
<span class="hint">Enter your full name</span>
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<span class="hint">We'll never share your email</span>
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" name="password">
<span class="hint">At least 8 characters</span>
</div>
</form>
const form = document.getElementById("user-form");
// One handler for all inputs (show hint on focus)
form.addEventListener("focusin", (event) => {
const field = event.target.closest(".field");
if (field) {
const hint = field.querySelector(".hint");
if (hint) hint.style.display = "block";
field.classList.add("active");
}
});
// One handler for all inputs (hide hint and validate on blur)
form.addEventListener("focusout", (event) => {
const field = event.target.closest(".field");
if (field) {
const hint = field.querySelector(".hint");
if (hint) hint.style.display = "none";
field.classList.remove("active");
// Validate the field that lost focus
validateField(event.target);
}
});
With focusin/focusout, you need only two event handlers on the form to manage focus behavior for any number of fields, including fields added dynamically later.
Detecting Focus Leaving a Container
A common need is detecting when focus leaves a group of elements entirely (for example, closing a dropdown when the user tabs out of it). focusout combined with relatedTarget handles this:
const dropdown = document.getElementById("dropdown-menu");
dropdown.addEventListener("focusout", (event) => {
// Check if the new focus target is still inside the dropdown
const newFocusTarget = event.relatedTarget;
if (!dropdown.contains(newFocusTarget)) {
// Focus has left the dropdown entirely
closeDropdown();
}
// Otherwise, focus just moved to another element inside the dropdown
});
Capture Phase Alternative
If you need to use focus/blur (not focusin/focusout) on a parent, you can use the capture phase:
// focus doesn't bubble, but it IS captured
container.addEventListener("focus", (event) => {
console.log("Focus captured:", event.target.name);
}, true); // Capture phase
container.addEventListener("blur", (event) => {
console.log("Blur captured:", event.target.name);
}, true); // Capture phase
This works because all events (including non-bubbling ones) travel through the capture phase. However, focusin/focusout are the cleaner solution for delegation.
Event Order
When focus moves from one element to another, the events fire in this order:
const input1 = document.getElementById("input1");
const input2 = document.getElementById("input2");
["focus", "blur", "focusin", "focusout"].forEach(type => {
input1.addEventListener(type, () => console.log(`input1: ${type}`));
input2.addEventListener(type, () => console.log(`input2: ${type}`));
});
When focus moves from input1 to input2:
input1: blur
input1: focusout
input2: focus
input2: focusin
The old element loses focus first (blur, focusout), then the new element gains focus (focus, focusin).
The exact order may vary slightly between browsers. The specification says blur and focusout fire first on the old element, then focus and focusin fire on the new element. Do not rely on a specific order between blur/focusout on the same element.
autofocus Attribute
The autofocus HTML attribute makes an element receive focus automatically when the page loads. Only one element per page should have this attribute.
Basic Usage
<!-- This input gets focus when the page loads -->
<input type="text" name="search" autofocus placeholder="Search...">
// You can also set it programmatically (though elem.focus() is more common)
const input = document.getElementById("search");
input.autofocus = true; // Only takes effect on page load, not after
Autofocus Limitations
- Only works on page load, not when elements are added dynamically
- Only one element should have
autofocus - Does not work in elements that are initially hidden (e.g., in
display: nonecontainers) - Can be disorienting for screen reader users if used carelessly
// For dynamically created elements, use elem.focus() instead
const newInput = document.createElement("input");
newInput.type = "text";
newInput.placeholder = "New field";
document.body.appendChild(newInput);
// autofocus won't work here (the page already loaded)
newInput.autofocus = true; // No effect
// Use focus() instead
newInput.focus(); // Works!
Autofocus in Modals and Dialogs
When opening a modal, you typically want to focus the first interactive element:
function openModal(modalElement) {
modalElement.hidden = false;
// Focus the first focusable element inside the modal
const firstFocusable = modalElement.querySelector(
'input, select, textarea, button, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
} else {
// If no focusable element, focus the modal itself
modalElement.setAttribute("tabindex", "-1");
modalElement.focus();
}
}
tabindex: Making Any Element Focusable
By default, only interactive elements can receive focus: <input>, <select>, <textarea>, <button>, <a href="...">. The tabindex attribute lets you make any element focusable and control the tab order.
tabindex Values
| Value | Behavior |
|---|---|
tabindex="0" | Element is focusable and participates in the normal tab order (in DOM order) |
tabindex="-1" | Element is focusable via JavaScript (elem.focus()) but skipped by Tab key |
tabindex="1" or higher | Element gets higher priority in tab order (use with extreme caution) |
tabindex="0": Adding to Tab Order
<!-- These are NOT focusable by default -->
<div>Not focusable</div>
<span>Not focusable</span>
<li>Not focusable</li>
<!-- Adding tabindex="0" makes them focusable -->
<div tabindex="0">Now I'm focusable!</div>
<span tabindex="0" role="button">Click me</span>
<li tabindex="0">Selectable item</li>
// These elements can now receive focus events
document.querySelector('[tabindex="0"]').addEventListener("focus", () => {
console.log("Custom element focused");
});
tabindex="-1": Programmatically Focusable Only
<div id="error-message" tabindex="-1" role="alert">
Please fix the errors below
</div>
// User cannot Tab to this element, but JavaScript can focus it
function showError(message) {
const errorDiv = document.getElementById("error-message");
errorDiv.textContent = message;
errorDiv.hidden = false;
errorDiv.focus(); // Works because tabindex="-1"
// Screen readers will announce this element
}
Common uses for tabindex="-1":
- Error messages that should be announced by screen readers
- Modal containers that need to receive focus
- Sections of the page you want to scroll to and announce
- Elements that should be focusable only in specific states
Positive tabindex: Generally Avoid
<!-- Positive tabindex values create custom tab order -->
<input tabindex="3" placeholder="Third">
<input tabindex="1" placeholder="First">
<input tabindex="2" placeholder="Second">
<input placeholder="Fourth (natural order)">
Tab order: First → Second → Third → Fourth
Avoid positive tabindex values. They create a confusing, hard-to-maintain tab order that deviates from the visual layout. Rearrange the DOM order instead. The only widely recommended values are 0 and -1.
Making Custom Interactive Components
When building custom UI components (tabs, menus, listboxes), use tabindex to make them keyboard accessible:
<div class="custom-select" tabindex="0" role="listbox" aria-label="Color">
<div class="option" tabindex="-1" role="option">Red</div>
<div class="option" tabindex="-1" role="option">Green</div>
<div class="option" tabindex="-1" role="option">Blue</div>
</div>
const customSelect = document.querySelector(".custom-select");
const options = customSelect.querySelectorAll(".option");
let activeIndex = 0;
customSelect.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowDown":
event.preventDefault();
activeIndex = Math.min(activeIndex + 1, options.length - 1);
options[activeIndex].focus();
break;
case "ArrowUp":
event.preventDefault();
activeIndex = Math.max(activeIndex - 1, 0);
options[activeIndex].focus();
break;
case "Enter":
case " ":
event.preventDefault();
selectOption(options[activeIndex]);
break;
}
});
tabindex via JavaScript
// Set tabindex programmatically
element.tabIndex = 0; // Make focusable in tab order
element.tabIndex = -1; // Make programmatically focusable only
// Read tabindex
console.log(element.tabIndex); // Returns the numeric value
// Note: the property is tabIndex (camelCase), the attribute is tabindex (lowercase)
element.setAttribute("tabindex", "0"); // Attribute
element.tabIndex = 0; // Property (both work)
elem.focus() and elem.blur() Methods
These methods programmatically set or remove focus.
elem.focus()
const input = document.getElementById("search");
// Give focus to the element
input.focus();
// Focus with options
input.focus({ preventScroll: true }); // Focus without scrolling to the element
The preventScroll option is useful when you want to focus an element that is off-screen without jumping the page to it.
elem.blur()
const input = document.getElementById("search");
// Remove focus from the element
input.blur();
// Focus moves to the document body (or nowhere specific)
Focus Only Works on Focusable Elements
Calling focus() on a non-focusable element silently does nothing:
const div = document.getElementById("regular-div");
div.focus(); // Nothing happens (divs are not focusable by default)
// Make it focusable first
div.tabIndex = -1;
div.focus(); // Now it works
Focus Requires Visibility
You cannot focus an element that is hidden or not rendered:
const input = document.getElementById("hidden-input");
input.style.display = "none";
input.focus(); // Does nothing (element is not rendered)
input.style.visibility = "hidden";
input.focus(); // Does nothing (element is invisible)
input.hidden = true;
input.focus(); // Does nothing (element is hidden)
// Must make visible first
input.style.display = "block";
input.hidden = false;
input.focus(); // Now works
Focus Timing with Dynamic Content
When adding elements to the DOM, you may need to wait a tick before focusing:
// Sometimes focus needs a microtask delay
const newInput = document.createElement("input");
document.body.appendChild(newInput);
newInput.focus(); // Usually works immediately
// But in some cases (animations, transitions, framework rendering):
requestAnimationFrame(() => {
newInput.focus(); // More reliable after render
});
// Or with a minimal timeout
setTimeout(() => {
newInput.focus();
}, 0);
Preventing Focus Loss
Sometimes you need to keep focus on a specific element (for example, in a search autocomplete):
const searchInput = document.getElementById("search");
const dropdown = document.getElementById("suggestions");
// When the user clicks a suggestion, focus would normally leave the input
dropdown.addEventListener("mousedown", (event) => {
event.preventDefault(); // Prevents the click from stealing focus from the input
// Handle the selection
const suggestion = event.target.closest(".suggestion");
if (suggestion) {
searchInput.value = suggestion.textContent;
dropdown.hidden = true;
}
});
The mousedown → preventDefault() trick is essential here. By default, clicking on the dropdown would blur the input (triggering your blur handler to close the dropdown) before the click handler runs. Preventing the default mousedown behavior prevents the focus change.
document.activeElement
The document.activeElement property returns the element that currently has focus. If no element has focus, it returns document.body or null.
Basic Usage
console.log(document.activeElement); // The currently focused element
// Common checks
if (document.activeElement === document.body) {
console.log("No specific element has focus");
}
if (document.activeElement.tagName === "INPUT") {
console.log("An input is focused:", document.activeElement.name);
}
Tracking Focus Changes
// Log every focus change
document.addEventListener("focusin", () => {
console.log("Active element:", document.activeElement.tagName, document.activeElement.id);
});
// Check if a specific element is focused
function isElementFocused(element) {
return document.activeElement === element;
}
Saving and Restoring Focus
A critical pattern for modals, dialogs, and overlays: save focus before opening, restore it after closing.
let previouslyFocused = null;
function openModal(modal) {
// Save the currently focused element
previouslyFocused = document.activeElement;
modal.hidden = false;
// Focus the first focusable element in the modal
const firstFocusable = modal.querySelector(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
}
}
function closeModal(modal) {
modal.hidden = true;
// Restore focus to where it was before the modal opened
if (previouslyFocused) {
previouslyFocused.focus();
previouslyFocused = null;
}
}
Checking Focus Within a Container
function hasFocusWithin(container) {
return container.contains(document.activeElement);
}
// Usage
const form = document.getElementById("my-form");
console.log(hasFocusWithin(form)); // true if any form element is focused
The CSS pseudo-class :focus-within provides the same check in CSS:
.form-group:focus-within {
border-color: blue;
background: #f0f8ff;
}
Focus Delegation Patterns
Combining focus events, tabindex, and document.activeElement enables powerful patterns for managing focus in complex interfaces.
Focus Trap for Modals
A focus trap keeps Tab and Shift+Tab cycling within a container, preventing the user from tabbing to elements behind a modal:
function trapFocus(container) {
const focusableSelector = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(", ");
const focusableElements = container.querySelectorAll(focusableSelector);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
function handleKeydown(event) {
if (event.key !== "Tab") return;
if (event.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === firstFocusable) {
event.preventDefault();
lastFocusable.focus();
}
} else {
// Tab: if on last element, wrap to first
if (document.activeElement === lastFocusable) {
event.preventDefault();
firstFocusable.focus();
}
}
}
container.addEventListener("keydown", handleKeydown);
// Return cleanup function
return () => {
container.removeEventListener("keydown", handleKeydown);
};
}
// Usage
const modal = document.getElementById("my-modal");
let releaseTrap;
function openModal() {
modal.hidden = false;
releaseTrap = trapFocus(modal);
modal.querySelector("input, button")?.focus();
}
function closeModal() {
modal.hidden = true;
if (releaseTrap) releaseTrap();
}
Roving Tabindex for Widget Navigation
The roving tabindex pattern makes a group of elements act as a single tab stop. Only one item has tabindex="0" at a time. Arrow keys move focus within the group:
<div class="toolbar" role="toolbar" aria-label="Text formatting">
<button tabindex="0" data-action="bold"><b>B</b></button>
<button tabindex="-1" data-action="italic"><i>I</i></button>
<button tabindex="-1" data-action="underline"><u>U</u></button>
<button tabindex="-1" data-action="strike"><s>S</s></button>
</div>
const toolbar = document.querySelector(".toolbar");
const buttons = [...toolbar.querySelectorAll("button")];
let currentIndex = 0;
toolbar.addEventListener("keydown", (event) => {
let newIndex = currentIndex;
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
event.preventDefault();
newIndex = (currentIndex + 1) % buttons.length;
break;
case "ArrowLeft":
case "ArrowUp":
event.preventDefault();
newIndex = (currentIndex - 1 + buttons.length) % buttons.length;
break;
case "Home":
event.preventDefault();
newIndex = 0;
break;
case "End":
event.preventDefault();
newIndex = buttons.length - 1;
break;
default:
return;
}
// Update roving tabindex
buttons[currentIndex].tabIndex = -1;
buttons[newIndex].tabIndex = 0;
buttons[newIndex].focus();
currentIndex = newIndex;
});
With this pattern, the toolbar takes one Tab stop. Once inside, arrow keys navigate between buttons. Tab moves to the next widget entirely.
Form Field Focus Flow with Validation
const form = document.getElementById("checkout-form");
form.addEventListener("focusout", (event) => {
const field = event.target;
if (!field.matches("input, select, textarea")) return;
// Validate on blur
const isValid = field.checkValidity();
const errorEl = field.closest(".field-group")?.querySelector(".error-message");
if (!isValid) {
field.classList.add("invalid");
field.setAttribute("aria-invalid", "true");
if (errorEl) {
errorEl.textContent = field.validationMessage;
errorEl.hidden = false;
field.setAttribute("aria-describedby", errorEl.id);
}
} else {
field.classList.remove("invalid");
field.removeAttribute("aria-invalid");
if (errorEl) {
errorEl.textContent = "";
errorEl.hidden = true;
}
}
});
// On submit, focus the first invalid field
form.addEventListener("submit", (event) => {
if (!form.checkValidity()) {
event.preventDefault();
const firstInvalid = form.querySelector(":invalid");
if (firstInvalid) {
firstInvalid.focus();
firstInvalid.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
});
Skip Links for Accessibility
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav><!-- Long navigation --></nav>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
.skip-link {
position: absolute;
top: -100px;
left: 0;
z-index: 1000;
}
.skip-link:focus {
top: 0;
padding: 8px 16px;
background: #000;
color: #fff;
}
document.querySelector(".skip-link").addEventListener("click", (event) => {
event.preventDefault();
const target = document.getElementById("main-content");
target.focus();
target.scrollIntoView({ behavior: "smooth" });
});
The skip link is invisible until the user tabs to it. Pressing Enter focuses the main content, skipping past the navigation for keyboard and screen reader users.
Summary
Focus management is both a UX feature and an accessibility requirement. The browser provides a complete API for tracking, controlling, and responding to focus changes, and using it properly makes your applications usable for everyone.
| Concept | Key Point |
|---|---|
focus event | Fires when an element receives focus. Does NOT bubble. |
blur event | Fires when an element loses focus. Does NOT bubble. |
focusin event | Same as focus but DOES bubble. Use for event delegation. |
focusout event | Same as blur but DOES bubble. Use for event delegation. |
event.relatedTarget | The other element in the focus change (what lost/gained focus). |
autofocus attribute | Focuses the element on page load. Only one per page. Does not work for dynamically added elements. |
tabindex="0" | Makes any element focusable and part of the tab order. |
tabindex="-1" | Makes any element focusable via JavaScript only (not Tab). |
Positive tabindex | Creates custom tab order. Avoid. Rearrange DOM instead. |
elem.focus() | Programmatically gives focus to an element. Element must be focusable and visible. |
elem.blur() | Programmatically removes focus from an element. |
preventScroll option | elem.focus({ preventScroll: true }) focuses without scrolling. |
document.activeElement | The currently focused element. Returns body or null when nothing is focused. |
mousedown + preventDefault | Prevents focus from moving when clicking an element (useful for dropdowns, autocompletes). |
| Focus trap | Keeps Tab cycling within a container (essential for modals). |
| Roving tabindex | One tab stop for a widget group, arrow keys navigate within. |
| Save/restore focus | Save document.activeElement before opening modals, restore on close. |
Key rules to remember:
- Use
focusin/focusoutfor event delegation, notfocus/blur(which do not bubble) - Always save and restore focus when opening and closing modals
- Use
tabindex="-1"on non-interactive elements you need to focus programmatically - Avoid positive
tabindexvalues; they create maintenance nightmares - Prevent
mousedowndefault to keep focus on inputs when the user clicks dropdown items - An element must be visible and rendered for
focus()to work - Use
:focus-withinin CSS for styling containers when any child has focus