What is focus management?

Focus management is the practice of programmatically controlling where keyboard focus goes in response to user actions. It's critical for:

Poor focus management can leave keyboard users lost on a page, unable to continue their task.

Focus indicators (WCAG 2.4.7)

Focus indicators show which element currently has keyboard focus.

Requirements

Recommended CSS approach

/* Use :focus-visible for keyboard-only focus */
:focus-visible {
  outline: 3px solid #0C234B;
  outline-offset: 2px;
}

/* Fallback for browsers without :focus-visible */
:focus:not(:focus-visible) {
  outline: none;
}

/* High contrast for dark backgrounds */
.dark-bg :focus-visible {
  outline-color: #FFD700;
}

Never do this

/* โŒ Removes all focus indicators */
*:focus {
  outline: none;
}

/* โŒ Hides focus on specific elements */
a:focus, button:focus {
  outline: 0;
}

Focus order (WCAG 2.4.3)

Focus order should match the visual reading order and preserve meaning.

Best practices

CSS layout considerations

/* Flexbox can reorder visually but not focus order */
.flex-container {
  display: flex;
  flex-direction: row-reverse; /* Visual order changed */
}
/* Focus still follows DOM order - may confuse users */

/* If you must reorder, consider matching tabindex or DOM order */

Focus not obscured (WCAG 2.4.11)

New in WCAG 2.2: When an element receives focus, it must not be entirely hidden by other content.

Common problems

Solutions

/* Use scroll-margin to offset for sticky headers */
[id] {
  scroll-margin-top: 80px; /* Height of sticky header */
}

/* Or scroll-padding on the container */
html {
  scroll-padding-top: 80px;
}

/* Ensure modals don't cover focused content behind them */
.modal-open {
  /* Trap focus inside modal instead */
}

Modal dialogs require careful focus management to be accessible.

Requirements

  1. On open: Move focus into the modal (first focusable element or modal itself)
  2. Focus trap: Keep focus inside modal while open
  3. On close: Return focus to the element that opened the modal
  4. Escape key: Close modal and return focus

Implementation pattern

// Store trigger element
let triggerElement;

function openModal(modal) {
  triggerElement = document.activeElement;
  modal.setAttribute('aria-hidden', 'false');
  modal.style.display = 'block';
  
  // Focus first focusable element or modal
  const firstFocusable = modal.querySelector(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  if (firstFocusable) {
    firstFocusable.focus();
  } else {
    modal.setAttribute('tabindex', '-1');
    modal.focus();
  }
}

function closeModal(modal) {
  modal.setAttribute('aria-hidden', 'true');
  modal.style.display = 'none';
  
  // Return focus to trigger
  if (triggerElement) {
    triggerElement.focus();
  }
}

Focus trap implementation

function trapFocus(modal) {
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  modal.addEventListener('keydown', function(e) {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      } else if (!e.shiftKey && document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
    if (e.key === 'Escape') {
      closeModal(modal);
    }
  });
}

Single-page application focus

SPAs don't trigger page loads, so focus management must be handled manually.

Route change patterns

Example: React route change

// Focus main content on route change
useEffect(() => {
  const mainContent = document.getElementById('maincontent');
  if (mainContent) {
    mainContent.tabIndex = -1;
    mainContent.focus();
    // Remove tabindex after focus to prevent re-focus issues
    mainContent.addEventListener('blur', () => {
      mainContent.removeAttribute('tabindex');
    }, { once: true });
  }
}, [location.pathname]);

Dynamic content focus

Form error handling

When form validation fails:

  1. Move focus to error summary or first error
  2. Announce errors to screen readers
  3. Link errors to specific fields
function handleFormErrors(errors) {
  // Create or update error summary
  const summary = document.getElementById('error-summary');
  summary.innerHTML = `<h2>${errors.length} errors found</h2>...`;
  
  // Focus error summary
  summary.tabIndex = -1;
  summary.focus();
  
  // Announce to screen readers
  summary.setAttribute('role', 'alert');
}

Infinite scroll / Load more

When new content loads:

Delete actions

When an item is deleted:

Testing focus management

Manual testing

  1. Tab through entire page without mouse
  2. Open/close modals and verify focus returns
  3. Trigger route changes and check focus
  4. Submit forms with errors
  5. Verify focus is never lost off-screen

Automated tools

Debug tip

// Log focus changes in console
document.addEventListener('focusin', (e) => {
  console.log('Focus moved to:', e.target);
});

Focus management checklist

Resources