Focus management, ARIA and testing

Moving focus deliberately in dynamic interfaces, using ARIA only when needed, and a testing routine that finds real barriers.

Managing focus in dynamic interfaces

When content appears or disappears without a page load, keyboard users can lose their place entirely. Focus rules for the common cases: opening a dialog moves focus into it and keeps it there; closing returns focus to the control that opened it; a newly revealed section receives focus; a removed element's focus moves somewhere sensible rather than back to the top of the document.

// dialog: trap focus inside, restore it on close
const dialog = document.querySelector("#dialog");
const opener = document.activeElement;

function open() {
  dialog.showModal();                 // native element handles the trap
  dialog.querySelector("input")?.focus();
}

function close() {
  dialog.close();
  opener?.focus();                    // give the user their place back
}

// a status message that assistive tech announces without stealing focus
status.textContent = "Draft saved";
// status element: <p id="status" role="status" aria-live="polite"></p>
  • role="alert" interrupts immediately - reserve it for errors, and use role="status" for confirmations.
  • Never move focus on a timer; it fights the user.
  • Anchor links to a section should also set tabindex="-1" on the target so focus follows the scroll.
  • Client-side route changes are invisible to assistive tech: announce the new page and move focus to its heading.

ARIA, when it is actually needed

The first rule of ARIA is not to use it. It changes how the accessibility tree is exposed, not how anything behaves - so it can only help where native semantics are genuinely missing, and it can only harm when it contradicts what the element really is.

AttributeUse it forWatch out
aria-labelNaming a control with no visible textIgnored on some generic elements; keep names short
aria-labelledbyNaming from existing visible textPoints at element ids, not text
aria-describedbyHint or error linked to an inputAnnounced in addition to the label, not instead of it
aria-expandedDisclosure buttons and menusMust be updated on every toggle
aria-hidden="true"Removing purely decorative duplicatesNever on focusable content
aria-liveRegions whose changes should be announcedSet it up before the content changes, not after
role="button"Almost nothing - prefer the elementDoes not add keyboard behaviour
<!-- good: native toggle with correct open state -->
<button type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>
  <li><a href="/orders">Orders</a></li>
</ul>

<!-- icon-only control needs an accessible name -->
<button type="button" aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false" viewBox="0 0 16 16">...</svg>
</button>

A testing routine that finds real problems

  1. Automated scan first: it clears the mechanical issues (missing labels, contrast, duplicate ids, invalid ARIA) in minutes.
  2. Keyboard-only pass: complete the main task with Tab, Enter, Space and Escape, watching the focus ring and reading the announcement order.
  3. Screen reader pass: one flow with NVDA on Windows or VoiceOver on macOS, at least checking that headings and form labels are announced correctly.
  4. Zoom and reflow: 200% zoom and a 320 px wide viewport with no lost content or two-dimensional scrolling.
  5. Reduced motion and other preferences: honour prefers-reduced-motion and confirm nothing depends on animation to be understood.
  6. Fix, then repeat on the template - a fixed component fixes every page that uses it.
# automated pass in CI, fails the build on new violations
npx axe https://localhost:3000 --exit

# or in a Playwright test
npx playwright test a11y.spec.js
💡
Test with the tools your users actually rely on, not only with the scanner. A clean automated report and an unusable keyboard flow is a very common combination.

FAQ

Does aria-hidden fix duplicate content?
It hides content from assistive technology, so use it for genuinely decorative copies. If the duplicated content is the real content, restructure the markup instead - and never apply it to anything focusable.
How often should accessibility be tested?
On every new component and on every change to a shared template. Retrofitting a whole site is far more expensive than checking a reusable component once.

Semantic HTML and keyboard access Semantic layout

Last refreshed 2026-09-18.