Blog/Software Development

Web Accessibility Best Practices for Developers

Laptop displaying lines of code on a desk in a modern office workspace.

Summarize with:

Every website and application we build is used by people with a wide range of abilities and preferences. Some navigate exclusively by keyboard, others rely on screen readers to hear what is on the page. Many adjust contrast, text size, or motion settings to make interfaces usable.

We as developers have a responsibility towards users to make our software such that they should be able to perceive, understand, and interact with it with relative ease. Most of that responsibility lives in the markup, behavior, and UI patterns we ship every day. It’s really about the habits behind our day to day work — the small, repeatable decisions we make while writing code, reviewing pull requests, and releasing features.

This guide covers that day-to-day workflow with a (no pun intended) focus on focus management, tooltips, developer quick wins, and ways to bring accessibility into a team’s entire development pipeline.

According to the WebAIM Million report, almost 96% of the top one million home pages had detectable WCAG 2 failures, averaging 56 accessibility errors per page. Many of those issues are not really edge cases, rather they come from missing labels, broken focus indicators and/or order and UI patterns implemented without keyboard users in mind. These are exactly the kind of problems this guide helps us prevent.

TL;DR

30-second summary

What are the day-to-day developer habits that fix the most common web accessibility failures?

  • Most accessibility failures trace back to a small set of recurring problems. The WebAIM Million report found 96% of the top one million home pages had detectable WCAG 2 failures, averaging 56 errors per page. Missing labels, broken focus order, and tooltip patterns implemented without keyboard users in mind account for a large share of these.
  • Focus management requires deciding where focus goes whenever the DOM changes. When a focused element like a clear button or a list item is removed, the browser defaults focus to <body>, causing users to lose their place. The fix is programmatically moving focus to a logical next element — back to the input after clearing it, or to the next list item after a deletion.
  • Tooltips and toggletips serve different purposes and need different ARIA wiring. Tooltips label or describe an element on hover and focus using aria-labelledby or aria-describedby, and must stay visible in the accessibility tree even when hidden visually. Toggletips reveal essential information on click and require a live region to announce that content, since it's initially unavailable to assistive technology.
  • Several low-effort techniques catch problems early in development. Watching document.activeElement in DevTools reveals where focus actually lands. Development-only CSS linter stylesheets flag missing alt text or unlabeled SVGs the moment they render. And outline-color: transparent should replace outline: none, since High Contrast mode suppresses box-shadow but still renders a transparent outline.
  • Lasting improvement requires accessibility at every pipeline stage, not just in code. Shift-left testing means catching issues during design collaboration, development, code review, and testing — supported by static analysis tools like eslint-plugin-jsx-a11y, runtime scanners like axe-core, and end-to-end checks like cypress-axe integrated into CI/CD.

Bottom line: Web accessibility best practices live in every decision across the development pipeline, from a designer's color choices to a developer's focus management code to the tools used to test and enforce it. Each individual fix is small, but the cumulative effect is significant, especially when 96% of the web still has room to grow.

Focus management

Keyboard users depend on a visible and predictable focus indicator to navigate websites and applications. The WCAG success criterion 2.4.3 Focus Order requires that focus moves in an order that preserves meaning and operability. Basically, it needs to match the visual layout of the page.

At a high level, this means avoiding tabindex values greater than 0.Positive tabindex values override the natural DOM order and create confusing navigation experiences for both sighted keyboard users and screen reader users.

At a finer grain, focus management means intentionally directing focus when the DOM changes. When an element disappears, the browser does not always know where focus should go next. The result is often what’s called a "dead focus" when focus lands on the <body> element and the user loses their place entirely. Let’s look at some examples that will help us understand this.

Clearable inputs

Imagine a text input with a "Clear" button that appears when the field has content in it. After a keyboard user presses that button, it gets removed from the DOM because the input is empty. But wait… the focus was on that button, so where will it go?

Well, in most browsers it will jump to <body>, but we don’t want that at all. We just want the user to clear the field and keep typing. The fix is simple: programmatically move focus back to the input after clearing it.

<div class="clearable-input">
  <input type="text" id="name" placeholder="Your name here" />
  <button class="invisible" aria-label="Clear">×</button>
</div>
input.addEventListener('input', () => {
  clearButton.classList.toggle('invisible', input.value.length === 0);
});

clearButton.addEventListener('click', () => {
  clearButton.classList.add('invisible');
  input.value = '';
  input.focus(); // Return focus to the input — don't leave it on <body>
});

Deletable list items

The same principle applies when removing items from a list. After deleting an item via keyboard, it will be removed from the DOM, so we need to move the focus to a logical next position:

  • Deleting the last item → focus the item before it (or an "Add item" button if the list now becomes empty)
  • Deleting any other item → focus the item after it

Which actual HTML element will receive focus depends on your UI. If the entire item is interactive, focus the entire item. If only a “Remove” button is focusable, focus that button.

<ul class="options-list">
  <li>Task 1 <button class="remove" aria-label="Remove item">×</button></li>
  <li>Task 2 <button class="remove" aria-label="Remove item">×</button></li>
  <li>Task 3 <button class="remove" aria-label="Remove item">×</button></li>
</ul>
<button>Add item</button>
optionsList.addEventListener('click', (e) => {
  const removeButton = e.target.closest('button.remove');
  const listElement = removeButton.parentElement;
  const idx = options.indexOf(listElement);
  if (idx === -1) return;

  let newIdx;
  if (idx === options.length - 1) {
    newIdx = Math.max(idx - 1, 0); // last item → focus previous
  } else {
    newIdx = idx + 1; // any other item → focus next
  }

  if (currentOptions.length === 1) {
    addItemButton.focus(); // Deleting the last remaining element
  } else {
    // This part depends on your UI
    options[newIdx].querySelector('button.remove')?.focus();
  }

  listElement.remove();
});

Here we are using event delegation to keep the listener count low. We have one handler on the list element instead of one per “Remove” button.

These two patterns — clearable inputs and deletable options — represent a broader rule: whenever our code removes a focused element from the DOM, we must decide where the focus goes next. If we do not, the browser will decide for us, and our users will pay the price.

Tooltips vs. Toggletips

Tooltips (“tips for tools”) are small popups that appear when a user hovers over or focuses an interactive element. They provide a label or a handy description without requiring an extra click.

Toggletips on the other hand are buttons whose sole purpose is to reveal hidden information on click. You will often see them next to form fields with complex requirements, likely as an "i" or "More info" icon that expands to show more context about the element next to it.

Both patterns show helpful text, but the trigger, ARIA wiring, and appropriate use cases are where they conceptually differ from each other and getting them right matters.

Tooltips as labels

We can use tooltips as primary labels only when there is absolutely no room (or need) for a visible label, like for example, an icon-only button. Connect the tooltip to the element being labeled with aria-labelledby, and give the tooltip element role="tooltip".

<button aria-labelledby="notifications-tooltip">
  <notification-bell-icon></notification-bell-icon>
</button>

<div id="notifications-tooltip" role="tooltip">
  Notifications
</div>

The tooltip must appear on both hover and keyboard focus, and it must stay open while the pointer moves from the button onto the tooltip itself. A short transition delay on show/hide gives users time to move their cursor without the tooltip disappearing.

For positioning, CSS anchor positioning is a modern alternative to JavaScript calculations or wrapper elements. Despite being a relatively new concept, it has baseline compatibility with all the major browsers and you can use it in production today.

button {
  anchor-name: --tooltip-anchor;

  & + [role='tooltip'] {
    position: absolute;
    position-anchor: --tooltip-anchor;
    top: anchor(center);
    left: anchor(right);
    opacity: 0;
    pointer-events: none;
  }
}

button:is(:hover, :focus-visible) + [role='tooltip'],
[role='tooltip']:hover {
  opacity: 1;
  pointer-events: all;
}

Tooltips as descriptions

When an element already has a label, we can use the tooltip to provide additional context using the aria-describedby attribute:

<button aria-label="Notifications" aria-describedby="notifications-tooltip">
  <notification-bell-icon></notification-bell-icon>
</button>

<div id="notifications-tooltip" role="tooltip">
   Check out the latest activity across your network
</div>

Notice how we only hide the tooltip through its opacity and pointer events. We don’t actually hide it from screen readers and that’s on purpose. We want users to be able to hear the label (or description) that the tooltip provides from a screen reader once they focus on the element that has the tooltip, so we keep the actual tooltip content in the DOM to be announced and just hide it visually.

Toggletips

Toggletips can (and should) only serve as descriptions, because essential information must remain visible without an extra click. Since the content of a toggletip is initially unavailable to assistive technologies, we need to utilize live regions to announce it when the user clicks on the toggletip:

<label for="emp-id">Employee ID</label>
<input id="emp-id" type="text" inputmode="numeric" placeholder="Employee ID">

<button aria-label="More info" data-toggletip="Last 4 digits of your birthday">
  i <div role="status"></div>
</button>
const liveRegion = toggletip.querySelector('[role="status"]');
const content = toggletip.getAttribute('data-toggletip');

toggletip.addEventListener('click', (e) => {
  liveRegion.textContent = e.target === toggletip ? content : '';
});

toggletip.addEventListener('blur', () => {
  liveRegion.textContent = '';
});

The most common mistake is using a hover tooltip where a toggletip is needed. Remember, a dedicated "More information" icon button calls for a toggletip. Contextual help that appears on hover or focus, without an extra button, calls for a tooltip.

Pattern Trigger ARIA connection Use case
Tooltip (label) Hover + focus aria-labelledby Interactive element with no label
Tooltip (description) Hover + focus aria-describedby Extra context for a labeled element
Toggletip Click Live region "More info" icon button

Quick wins you can use today

Aside from component-level patterns that improve the accessibility of our websites, there are several low-effort techniques that can help us catch problems early and make our debugging faster and easier.

Track focus with document.activeElement

During development, add a Live Expression in Chrome’s DevTools watching for the variable document.activeElement. This variable shows which element currently holds focus, which can be invaluable when debugging modals, drawers, and hidden menus where the focus ring disappears off-screen.

CSS "linter" stylesheets

Automated linters catch many issues, but a development-only CSS file can flag obvious mistakes the moment they render:

// Images missing alt text
img:not([alt]) {
  outline: 6px solid red !important;
  outline-offset: 2px;
}

// SVG icons missing accessible names
svg:not([aria-hidden='true']):not([aria-label]):not([aria-labelledby]) {
  outline: 6px solid darkorange !important;
  outline-offset: 2px;
}

You can extend this pattern to flag <div> elements acting as buttons, positive tabindex values, or <a> tags without href. Be careful with these though, rules like these can quickly spiral into giant CSS files that are hard to maintain, so try your best to strike a balance between enforcing actually useful rules and not overcrowding the CSS.

Visually hidden text

.sr-only (“Screen Reader only”) is a well known CSS class, present in all major CSS frameworks and it looks like this. It basically hides content visually while keeping it in the accessibility tree. Use it to add context that sighted users get from layout or iconography:

<div>
  <div class="spinner" aria-hidden="true"></div>
  <span class="sr-only">Loading images...</span>
</div>

<a href="https://testdevlab.com">
  Read more <span class="sr-only">about the company I work at</span>
</a>

This pairs well with the form labeling techniques covered in Building Accessible Forms on the Web, where we talk about establishing programmatic associations between labels and inputs.

Avoid outline: none

Custom focus indicators using box-shadow work well until someone enables High Contrast mode (forced colors). In that mode, box-shadow values are suppressed. If we also removed the outline with outline: none, the user would see no focus indicator at all.

Prefer outline-color: transparent over outline: none. This hides the outline in normal mode but allows the browser to render it in High Contrast mode.

DevTools accessibility features

Chromium DevTools can emulate Reduced Motion, High Contrast palettes, and color vision deficiencies like Protanopia, Deuteranopia, Tritanopia and Achromatopsia.

Most modern browsers also expose the accessibility tree alongside the DOM in the Inspect panel, showing exactly how assistive technologies interpret our markup, including roles, names, and hidden elements.

Automated scanners catch only a fraction of accessibility issues

Our accessibility testing services combine automated scanning with screen reader, keyboard-only, and real-user testing across the WCAG success criteria that tools alone can't fully verify.

Move accessibility beyond our IDE

As much as the individual developer habits matter, lasting improvement requires for the notion of accessibility to be present at every stage of the pipeline:

Design → Development → Code Review → Testing → Deployment

The answer to "where does accessibility belong in the development pipeline?" is everywhere. That is the idea behind shift-left testing, catching and fixing accessibility issues as early as possible, when they are cheapest to resolve.

During the design phase, collaborate with designers on color contrast, focus indicator visibility, animation preferences, font sizes, spacing, and error states. Ask questions early rather than discovering blockers during implementation.

During the development phase, choose native HTML elements and utilities, understand every ARIA attribute you add, and cover keyboard interactions and focus flows.

During the testing phase, include accessibility requirements in each feature's definition of done. Testers should verify keyboard navigation, screen reader announcements, and focus behavior alongside visual appearance.

During the code review and deployment phases, integrate automated accessibility checks into your local workflows and CI/CD pipeline:

Framework Static analysis Runtime scanning
React eslint-plugin-jsx-a11y @axe-core/react
Vue eslint-plugin-vuejs-accessibility vue-axe-next
Angular @angular-eslint/eslint-plugin-template axe-core
Svelte Built-in compiler warnings

For end-to-end testing, consider @axe-core/playwright or cypress-axe. Browser extensions like Lighthouse, Axe DevTools, and WAVE provide quick sanity checks during development. See Lighthouse's overview to learn how automated audits work. Use multiple tools as no single scanner can catch everything.

Two team-level practices worth mentioning:

  • No Mouse Days (The A11y Project): designate one day per week where the team navigates only by keyboard. The npm package no-mouse-days can hide the cursor in local environments to reinforce the habit.
  • Knowledge sharing: document accessible usage patterns for your design system, run short internal training sessions, and lead with empathy when raising Accessibility concerns with colleagues.

Conclusion

Web accessibility best practices live in every decision we make throughout the entire development pipeline, from the designers’ choices of colors to the developers code, all the way up to the tools we use to test for and enforce these practices.

Start with one change this week. Fix the focus flow on a clearable input. Add a CSS linter stylesheet to your dev environment. Each improvement is small, but the cumulative effect is significant, especially when 96% of the Web still has room to grow.

FAQ

Most common questions

What is focus management and why does it matter for accessibility?

Focus management means intentionally directing keyboard focus whenever the DOM changes, rather than letting the browser decide. When a focused element like a "Clear" button or a list item is removed, focus defaults to <body>, causing keyboard users to lose their place entirely — a problem known as dead focus. WCAG 2.4.3 requires that focus order preserve meaning and match the visual layout of the page, which is why positive tabindex values should be avoided since they override natural DOM order.

What is the difference between a tooltip and a toggletip?

A tooltip appears on hover or keyboard focus and provides a label or description without requiring a click, connected to its target using aria-labelledby for labels or aria-describedby for extra context. A toggletip is a button that reveals essential information only on click, and because that content is initially unavailable to assistive technology, it requires a live region to announce it when triggered. The most common mistake is using a hover tooltip where essential information actually needs a toggletip, or vice versa.

Why should developers avoid using outline: none for focus indicators?

Custom focus indicators built with box-shadow work in normal browser conditions, but High Contrast mode (forced colors) suppresses box-shadow values entirely. If outline: none has also been used to hide the default focus ring, the result is no visible focus indicator at all in High Contrast mode. Using outline-color: transparent instead hides the outline visually under normal conditions while still allowing the browser to render it when High Contrast mode is active.

What tools help catch accessibility issues during development?

Several low-effort techniques catch issues early. Watching document.activeElement as a Live Expression in Chrome DevTools reveals exactly where focus lands, which is useful when debugging modals and hidden menus. Development-only CSS linter stylesheets can visually flag missing alt text or unlabeled SVG icons the moment they render. Browser extensions like Lighthouse, Axe DevTools, and WAVE provide quick sanity checks, and Chromium DevTools can emulate Reduced Motion, High Contrast palettes, and color vision deficiencies directly in the Inspect panel.

How should accessibility be integrated into a development pipeline?

Accessibility should be addressed at every stage rather than left to a final testing pass, which is a practice known as shift-left testing. During design, this means collaborating on color contrast, focus indicator visibility, and error states. During development, it means choosing native HTML elements and understanding every ARIA attribute added. During testing, it means including accessibility requirements in each feature's definition of done. And during code review and deployment, static analysis tools like eslint-plugin-jsx-a11y and runtime scanners like axe-core should run automatically in CI/CD pipelines.

Accessible by design or accessible by accident?

We help engineering teams build accessibility into the full pipeline, from design review through CI/CD checks to full WCAG audits with real assistive technology users.

Summarize with:

QA engineer having a video call with 5-start rating graphic displayed above

Save your team from late-night firefighting

Stop scrambling for fixes. Prevent unexpected bugs and keep your releases smooth with our comprehensive QA services.

Explore our services