Accessibility basics for web developers

Article
Read time: 6 minutes

This article provides practical guidance for experienced developers on how to build accessible web applications. It covers how to recognize, prioritize, and fix the most common and critical accessibility issues in line with WCAG 2.1 AA standards.

Why accessibility matters

Accessibility is the practice of ensuring your digital products are usable by everyone, regardless of their abilities. It's not just a compliance requirement; it's a core component of creating high-quality products.

  • Human impact: Approximately 1 in 4 adults in the U.S. has a disability. Inaccessible products can exclude them from essential services, information, and opportunities.
  • Business impact: An accessible product reaches a larger audience, improves the user experience for everyone, and enhances your brand's reputation.
  • Legal impact: Laws like the Americans with Disabilities Act (ADA) require digital properties to be accessible, and non-compliance can carry legal risks.

Core principles: The critical four

Focus on these four areas to prevent the most significant barriers for users.

Keyboard accessibility

This is the foundation of an accessible website. If a component works with a mouse, it must also work with a keyboard.

  • Visible focus: A clear visual indicator must always be present on the interactive element that currently has keyboard focus. Never use outline: none; without providing a highly visible alternative style for the :focus state.
  • Keyboard traps: A user must be able to navigate into and, crucially, out of any component using only the keyboard. A common failure is trapping users inside a modal window.
Semantic HTML

Using HTML elements for their intended purpose provides a huge amount of accessibility for free. Screen readers rely on the HTML structure to give users context and navigational shortcuts.

  • Headings (<h1>-<h6>): Use headings to create a logical outline for your page content. Do not skip heading levels (e.g., jumping from an <h1> to an <h3>), as this disrupts the document structure for screen reader users.
  • Buttons vs. links: A link (<a>) navigates to a new location. A button (<button>) performs an action on the current page. Using a <div> with a click handler is not an acceptable substitute for a <button>, as it lacks the built-in focus behavior and semantics.
  • Landmarks (<main>, <nav>, <header>, <footer>): Use these elements to define the major regions of your page. This allows assistive technology users to jump directly to the section they need.

It’s important to note that HTML elements have specific attributes and parent/child relationships that are allowed. If in doubt, refer to the Mozilla elements reference site.

Mozilla elements reference

Alternative text (alt attribute) provides a textual description of an image for users who cannot see it.

  • Informative images: If an image conveys information (e.g., a chart, a product photo), the alt text must describe its content and purpose. (e.g., alt="Red running shoe with white laces, side view.").
  • Decorative images: If an image is purely decorative and adds no informational value, it must still have an alt attribute, but it should be empty (alt=""). This tells the screen reader to ignore it.
Programmatic labels and names

Every interactive control (inputs, buttons, links) must have a name that assistive technology can announce.

  • Form inputs: The element, correctly associated with an input via the for and id attributes, is the standard and best way to provide a name. A placeholder is not a substitute for a label.
  • Icon-only buttons: An icon by itself is meaningless to a screen reader. You must provide a programmatic name using aria-label or visually hidden text.

Essential tools 🛠️

Integrate these tools into your development workflow to efficiently find and fix issues.

  • Automated checkers: Browser extensions like axe DevTools are excellent for catching about 30-50% of common issues, such as most color contrast failures or missing form labels. The axe DevTools extension is built on axe-core and rarely has false positives. Do not rely on WAVE. It often has false positives.
  • Manual checks: Your keyboard is your most powerful testing tool. Navigate through your page using only the keyboard (Tab, Shift+Tab, Enter, Space, arrow keys). Can you see where you are and operate every control? The ANDI accessibility tool provides valuable information as you test manually.
  • Assistive technology: The best way to understand the user experience is to test with a screen reader. NVDA (free for Windows) and VoiceOver (built-in on macOS/iOS) are the most common.
  • Color contrast: The WebAIM Contrast Checker can be used from a website and does not require an installation on your system. It is also free to use.

Common mistakes and best practices

Hiding content correctly: aria-hidden vs. CSS

How you hide off-screen content (like in modals or accordions) has major accessibility implications.

  • display: none;: This is the correct choice for most use cases. It hides the element visually, removes it from the page layout, and hides it from screen readers.
  • aria-hidden="true": This only hides an element from the accessibility tree (screen readers). The element remains visible and can still be interactive.
The danger zone ⚠️

Never use aria-hidden="true" on a focusable, interactive element. A sighted keyboard user can still tab to it, but a screen reader user will skip right over it, creating a completely broken experience.

The golden rule for hiding content:
  • To hide something completely from all users: Use display: none;.
  • To hide something from screen readers only (because it is purely decorative or redundant to adjacent text): Use aria-hidden="true".
Property Visually hidden? Removed from layout? Hidden from screen reader? Interactive?
display: none; Yes Yes Yes No
visibility: hidden; Yes No Yes No
aria-hidden="true"; No No Yes Yes
 
Providing context with ARIA (aria-label, labelledby, describedby)

Use ARIA to fill semantic gaps, not to duplicate information that is already present and correctly associated (e.g., a button with visible text, a properly linked <label>).

  • aria-label: Provides a string as the accessible name when no visible label exists. It overrides any text content inside the element. This is perfect for icon-only buttons.
HTML
&amp;amp;amp;amp;amp;amp;amp;lt;button aria-label="Settings"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;svg&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;/svg&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;/button&amp;amp;amp;amp;amp;amp;amp;gt; aria-labelledby: Uses the ID of another visible element on the page as the accessible name. This is ideal for associating a modal dialog with its visible title.
HTML
&amp;amp;amp;amp;amp;amp;amp;lt;div role="dialog" aria-labelledby="dialog-title"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;h2 id="dialog-title"&amp;amp;amp;amp;amp;amp;amp;gt;Confirm Deletion&amp;amp;amp;amp;amp;amp;amp;lt;/h2&amp;amp;amp;amp;amp;amp;amp;gt; ... &amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; aria-describedby: Uses the ID of another element to provide a supplementary description (not the name). This is used for helpful hints or error messages connected to an input.
HTML
&amp;amp;amp;amp;amp;amp;amp;lt;label for="password"&amp;amp;amp;amp;amp;amp;amp;gt;Password&amp;amp;amp;amp;amp;amp;amp;lt;/label&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;input type="password" id="password" aria-describedby="pw-hint"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;p id="pw-hint"&amp;amp;amp;amp;amp;amp;amp;gt;Must be at least 8 characters long.&amp;amp;amp;amp;amp;amp;amp;lt;/p&amp;amp;amp;amp;amp;amp;amp;gt;

Accessibility in data visualizations

For data-heavy applications, ensuring reports are accessible is critical.

  • Use high-contrast colors or patterns with a legend: Start with a high-contrast theme from the View tab.
  • Add alt text to visuals: Go to Format → General → Alt Text and describe the chart's purpose and key insight.
  • Set a logical tab order: Use the View → Selection panel to drag elements into a logical top-to-bottom, left-to-right order.
  • Avoid relying on color alone: Use patterns, markers, and direct data labels in addition to color to convey information.
  • Avoid using tables for everything: Nested tables are very difficult or impossible to navigate with a screen reader.

Workflow: Prioritizing and documenting issues

Once issues are found, use Severity and Reach to prioritize them.

  • Severity: How much does this block a user from completing a task? (Critical, Serious, Moderate, Minor).
  • Reach: How many users will encounter this? (High, Medium, Low).

A Critical issue with High Reach (e.g., a keyboard trap on the login form) is always the highest priority.

Writing an effective Jira ticket

A good ticket accelerates the fix. Include these five elements:

  1. Clear, Actionable Title: "Login Modal is a Keyboard Trap"
  2. Location: URL the issue occurred on
  3. Severity Level: Critical, Serious, Moderate, or Minor
  4. Reach: High, Medium, Low
  5. User impact: "Keyboard-only users cannot access the login form fields."
  6. Steps to Reproduce (STR): Clear, numbered steps.
  7. xpected vs. Actual Results: What should happen vs. what currently happens.
  8. Acceptance Criteria: A clear definition of "done."

Practical example: Finding and fixing bugs

This case study demonstrates how to apply these principles. Use your IDE or a site like CodePen to enter the code and see the results.

The problematic component ("before")

This component uses non-semantic elements, is missing key attributes, and has CSS that harms accessibility.

HTML
&amp;amp;amp;amp;amp;amp;amp;lt;div class="card"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;h3&amp;amp;amp;amp;amp;amp;amp;gt;Jane Doe&amp;amp;amp;amp;amp;amp;amp;lt;/h3&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;img src="jane.jpg"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;p&amp;amp;amp;amp;amp;amp;amp;gt;Lead Software Engineer&amp;amp;amp;amp;amp;amp;amp;lt;/p&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;div onclick="openModal()" class="button-imitation"&amp;amp;amp;amp;amp;amp;amp;gt;Contact Me&amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;div id="contactModal" class="modal" role="dialog" style="display:none;"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;span class="modal-title"&amp;amp;amp;amp;amp;amp;amp;gt;Contact Jane&amp;amp;amp;amp;amp;amp;amp;lt;/span&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;input type="text" placeholder="Your email"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;textarea placeholder="Your message"&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;/textarea&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;div id='RadEditorStyleKeeper1' style='display:none;'&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;div id='RadEditorStyleKeeper3' style='display:none;'&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;style reoriginalpositionmarker='RadEditorStyleKeeper3' reoriginalpositionmarker='RadEditorStyleKeeper1'&amp;amp;amp;amp;amp;amp;amp;gt; /* Issue: Hides focus indicator for all users */ :focus { outline: none; } &amp;amp;amp;amp;amp;amp;amp;lt;/style&amp;amp;amp;amp;amp;amp;amp;gt;
 
The accessible solution ("after")

The fixes use semantic HTML, ARIA for context, and accessibility-friendly CSS and JavaScript.

HTML
&amp;amp;amp;amp;amp;amp;amp;lt;div class="card"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;h2&amp;amp;amp;amp;amp;amp;amp;gt;Jane Doe&amp;amp;amp;amp;amp;amp;amp;lt;/h2&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;img src="https://th.bing.com/th/id/R.ab41f64459c1958dede9054138c3d8c0?rik=BIEqLB2dYX0tDw&amp;amp;amp;amp;amp;amp;amp;amp;riu=http%3a%2f%2fimages2.fanpop.com%2fimages%2fphotos%2f2800000%2fjane-walt-disneys-tarzan-2879908-800-600.jpg&amp;amp;amp;amp;amp;amp;amp;amp;ehk=3H0arbXSlgcyWondf1rRWFQpgnw7B8Yo6BRAnlVbtVM%3d&amp;amp;amp;amp;amp;amp;amp;amp;risl=&amp;amp;amp;amp;amp;amp;amp;amp;pid=ImgRaw&amp;amp;amp;amp;amp;amp;amp;amp;r=0" alt="Portrait of Jane Doe"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;p&amp;amp;amp;amp;amp;amp;amp;gt;Lead Software Engineer&amp;amp;amp;amp;amp;amp;amp;lt;/p&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;button type="button" onclick="openModal()" class="button-imitation"&amp;amp;amp;amp;amp;amp;amp;gt;Contact Me&amp;amp;amp;amp;amp;amp;amp;lt;/button&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;div id="contactModal" class="modal" role="dialog" aria-labelledby="contactModalTitle" aria-modal="true" style="display:none;"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;h2 id="contactModalTitle" class="modal-title"&amp;amp;amp;amp;amp;amp;amp;gt;Contact Jane&amp;amp;amp;amp;amp;amp;amp;lt;/h2&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;label for="emailInput"&amp;amp;amp;amp;amp;amp;amp;gt;Your email &amp;amp;amp;amp;amp;amp;amp;lt;/label&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;input id="emailInput" type="email" placeholder="Your email"&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;br&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;br&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;label for="messageInput"&amp;amp;amp;amp;amp;amp;amp;gt;Your message &amp;amp;amp;amp;amp;amp;amp;lt;/label&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;textarea id="messageInput" placeholder="Your message"&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;/textarea&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;br&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;br&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;button type="button" class="button-imitation"&amp;amp;amp;amp;amp;amp;amp;gt;Send&amp;amp;amp;amp;amp;amp;amp;lt;/button&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;button type="button" class="close-btn" onclick="closeModal()" aria-label="Close contact form"&amp;amp;amp;amp;amp;amp;amp;gt;X&amp;amp;amp;amp;amp;amp;amp;lt;/button&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;div id='RadEditorStyleKeeper2' style='display:none;'&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;div id='RadEditorStyleKeeper4' style='display:none;'&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;lt;style reoriginalpositionmarker='RadEditorStyleKeeper4' reoriginalpositionmarker='RadEditorStyleKeeper2'&amp;amp;amp;amp;amp;amp;amp;gt; /* Restore visible focus outlines for accessibility */ :focus { outline: 2px solid #005fcc; outline-offset: 2px; } .button-imitation { display: inline-block; padding: 0.5em 1em; background: #005fcc; color: #fff; border: none; border-radius: 0.25em; cursor: pointer; font-size: 1em; } .button-imitation:hover, .button-imitation:focus { background: #0041a8; } .modal { background: #fff; border: 1px solid #ccc; padding: 1em; max-width: 400px; } img { width:10%; } &amp;amp;amp;amp;amp;amp;amp;lt;/style&amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;lt;!--RADEDITORSAVEDTAG_script&amp;amp;amp;amp;amp;amp;amp;gt; function openModal() { document.getElementById('contactModal').style.display = 'block'; document.getElementById('emailInput').focus(); } function closeModal() { document.getElementById('contactModal').style.display = 'none'; } &amp;amp;amp;amp;amp;amp;amp;lt;/script--&amp;amp;amp;amp;amp;amp;amp;gt; Download the guide in PDF (512 KB)

Fix accessibility issues