Focus management is one of the most commonly missed accessibility requirements in modern JavaScript applications. Server-rendered sites handle most of it naturally (page loads reset focus to the document). SPAs do not — every dynamic interaction requires explicit focus handling.
// Focus a naturally focusable element (button, input, link)
document.getElementById('close-btn').focus();
// Focus a non-interactive element (div, h2, p)
// Requires tabindex="-1" first
const heading = document.getElementById('page-title');
heading.setAttribute('tabindex', '-1');
heading.focus();
// Or set it in HTML
// <h1 id="page-title" tabindex="-1">...</h1>
// React Router example
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
function PageWrapper({ title, children }) {
const headingRef = useRef(null);
const location = useLocation();
useEffect(() => {
// Wait for DOM to update, then move focus
requestAnimationFrame(() => {
if (headingRef.current) {
headingRef.current.focus();
}
// Also update page title
document.title = `${title} | Accessalyze`;
});
}, [location.pathname]);
return (
<>
<h1 ref={headingRef} tabIndex={-1}>{title}</h1>
{children}
</>
);
}
// Move focus to error summary when validation fails
function handleSubmit(e) {
e.preventDefault();
const errors = validate(formData);
if (errors.length > 0) {
renderErrors(errors);
// Wait for DOM update, then focus error summary
requestAnimationFrame(() => {
const summary = document.getElementById('error-summary');
summary.setAttribute('tabindex', '-1');
summary.focus();
});
return;
}
// Success path
submitForm();
}
<div id="error-summary" role="alert" tabindex="-1">
<h2>Please fix 2 errors before continuing:</h2>
<ul>
<li><a href="#email">Email is required</a></li>
<li><a href="#password">Password must be at least 8 characters</a></li>
</ul>
</div>
function deleteItem(itemId, items) {
const currentIndex = items.findIndex(i => i.id === itemId);
const newItems = items.filter(i => i.id !== itemId);
setItems(newItems);
// Move focus to next item, or previous if last, or container if empty
requestAnimationFrame(() => {
if (newItems.length === 0) {
document.getElementById('items-list-container').focus();
} else {
const nextIndex = Math.min(currentIndex, newItems.length - 1);
document.querySelector(`[data-item-id="${newItems[nextIndex].id}"] button`).focus();
}
});
}
async function loadSearchResults(query) {
setLoading(true);
const results = await fetchResults(query);
setResults(results);
setLoading(false);
// After content loads, move focus to results region
requestAnimationFrame(() => {
const resultsContainer = document.getElementById('search-results');
resultsContainer.setAttribute('tabindex', '-1');
resultsContainer.focus();
// OR just update an aria-live region (polite)
document.getElementById('results-count').textContent =
`${results.length} results for "${query}"`;
});
}
.focus() before the element is in the DOM (use requestAnimationFrame or a callback)Accessalyze scans for WCAG violations including focus management issues in your JavaScript-rendered pages.
See how 321 websites scored →
View the 2026 ReportSee real website accessibility scores: Browse 244+ free accessibility audits →
Try it yourself
Enter your website URL to get a free accessibility score.