← Back to Accessalyze

By Genesis AI Services · April 21, 2026 · 8 min read · JavaScript

Focus Management in JavaScript

The rule: Whenever you change what's visible on screen in a meaningful way — opening a modal, navigating to a new route, revealing an error, deleting a list item — keyboard and screen reader users need focus to end up somewhere logical. If you don't explicitly manage focus, it often ends up in the wrong place or nowhere at all.

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.

When to Move Focus

  1. Modal opens → move focus to first focusable element inside modal (or modal container)
  2. Modal closes → return focus to the element that triggered the modal
  3. Route change (SPA) → move focus to page heading or main content region
  4. Form validation errors appear → move focus to the error summary at the top
  5. Form submitted successfully → move focus to success message
  6. Item deleted from list → move focus to the next/previous item, or to the list container if empty
  7. Inline edit completed → return focus to the item being edited
  8. Loading state completes → move focus to the loaded content

The Mechanics: .focus() and tabindex

// 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>
tabindex="-1" makes any element programmatically focusable without adding it to the Tab sequence. Use it on focus targets that aren't naturally interactive (headings, divs, sections).

SPA Route Changes

// 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}
    </>
  );
}

Form Validation Error Focus

// 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>

Deleting Items From a List

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();
    }
  });
}

Loading States

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}"`;
  });
}

Common Focus Management Mistakes

Find Accessibility Issues on Your Site

Accessalyze scans for WCAG violations including focus management issues in your JavaScript-rendered pages.

See how 321 websites scored →

View the 2026 Report
Run Free Accessibility Scan →

← Back to Accessalyze

Accessalyze - Free WCAG 2.1 scanner that writes the fix code for you | Product Hunt

See real website accessibility scores: Browse 244+ free accessibility audits →

Try it yourself

Enter your website URL to get a free accessibility score.

Check your website accessibility score free Scan Now →