Header Ads

Practical Angular and Ionic accessibility guide for ADA and WCAG 2.1 AA: semantic HTML, forms, focus, contrast, ARIA labels and testing with axe.

How do you make an Angular or Ionic app accessible for people with disabilities and ready for ADA and WCAG 2.1 AA expectations? This practical guide shows 10 fixes with code: semantic HTML, forms, focus, color contrast, screen reader labels, zoom, motion and automated testing.

Accessibility is easy to postpone, because an app that looks fine and passes your tests can still be unusable with a keyboard, a screen reader or a zoomed-in screen. It is also much cheaper to build in from the start than to repair later. This guide focuses on what you, as an Angular or Ionic developer, can change in your code.

What ADA and WCAG Mean for Developers

The Americans with Disabilities Act (ADA) is a United States civil rights law. It does not contain a list of technical rules for websites and apps, so developers usually work with the Web Content Accessibility Guidelines (WCAG), published by the W3C. WCAG is organized around four ideas: content must be perceivable, operable, understandable and robust. Each guideline has levels, and Level AA is the target most organizations and rules use.

  • In 2024, the U.S. Department of Justice adopted WCAG 2.1 Level AA as the technical standard for websites and mobile apps of state and local governments. In April 2026, it extended the compliance deadlines for that rule by one year.
  • For private businesses, WCAG 2.1 AA is still widely used as the practical benchmark, and WCAG 2.2 is a newer version that adds more criteria.
  • Other regions have their own laws and standards, such as the European Accessibility Act in the EU and accessibility guidelines for government sites in India.

I am a developer, not a lawyer, and this post is not legal advice. Check the rules that apply to your country, your clients and your type of organization. No tool or checklist can guarantee compliance, but the steps below remove many of the most common barriers.


Step 1: Use the Right HTML Element

The biggest accessibility win is using real HTML elements. A real <button> is focusable, works with the keyboard (Enter and Space) and is announced as a button by screen readers. A clickable <div> does none of that:

<!-- bad: not focusable, no keyboard support, no role -->
<div class="btn" (click)="save()">Save</div>

<!-- good -->
<button type="button" (click)="save()">Save</button>

<!-- good: navigation is a link, not a button -->
<a routerLink="/orders">Orders</a>

In Ionic, <ion-button> renders a native button, so it is fine. For list rows that open something, add the button attribute so ion-item renders a focusable element:

<ion-item button (click)="openOrder(order)">{{ order.name }}</ion-item>

Also use headings in order (h1, then h2, and so on), and landmarks such as <main>, <nav> and <header>, because screen reader users jump between them.


Step 2: Label Icons, Images and Icon-Only Buttons

Every image needs an alt text that describes its purpose, or an empty alt="" if it is purely decorative. A button that has only an icon needs a text label for screen readers:

<img src="assets/logo.png" alt="My App logo" />
<img src="assets/divider.png" alt="" />

<ion-button aria-label="Delete order" (click)="remove()">
<ion-icon slot="icon-only" name="trash"></ion-icon>
</ion-button>

Without the label, a screen reader may announce only “button”, and the user has no idea what it does. For plain HTML buttons with icons, use the same aria-label, or add visually hidden text.


Step 3: Make Forms Understandable

Each input needs a label that is connected to it, and error messages must be available to assistive technology, not only shown in red:

<label for="email">Email</label>
<input id="email" type="email" formControlName="email"
[attr.aria-invalid]="form.controls.email.invalid && form.controls.email.touched"
aria-describedby="email-error" />

<p id="email-error" role="alert"
*ngIf="form.controls.email.invalid && form.controls.email.touched">
Enter a valid email address.
</p>

In Ionic 7 and newer, you can set the label directly on the component:

<ion-input label="Email" labelPlacement="floating" type="email" formControlName="email"></ion-input>

Do not use a placeholder as the only label, because it disappears when the user types. Also mark required fields in text, not only with a colored asterisk.


Step 4: Make Everything Work With the Keyboard

  • Press Tab through your whole app. Every interactive element should be reachable, in a logical order.
  • Never remove the focus outline (outline: none) without adding a clear replacement style.
  • Avoid positive tabindex values such as tabindex="5". They break the natural order.
  • Make sure modals and menus can be closed with the Escape key, and that focus does not escape behind them.
:focus-visible {
outline: 3px solid #1a5fb4;
outline-offset: 2px;
}

Angular provides helpers in the CDK. Install it with npm install @angular/cdk and use cdkTrapFocus for custom dialogs that Ionic does not already handle. Ionic’s own modals, popovers and action sheets include focus handling, so test them rather than rebuilding them.


Step 5: Manage Focus and Page Titles on Navigation

In a single-page app, clicking a link does not reload the page, so a screen reader user may not know anything changed. Give every route a unique title, which Angular supports directly:

export const routes: Routes = [
{ path: 'orders', component: OrdersComponent, title: 'Orders | My App' },
{ path: 'profile', component: ProfileComponent, title: 'Profile | My App' }
];

Then move focus to the main content after navigation, for example in a plain Angular app:

import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs';

constructor(private router: Router) {
this.router.events
.pipe(filter((e) => e instanceof NavigationEnd))
.subscribe(() => document.getElementById('main-content')?.focus());
}

// in the template
// <main id="main-content" tabindex="-1">...</main>

Ionic’s router outlet already handles part of page transitions, so test with a screen reader before adding extra focus code to an Ionic app, to avoid announcing things twice.

For messages that appear without navigation, such as “Order saved”, use the CDK live announcer so screen readers speak them:

import { LiveAnnouncer } from '@angular/cdk/a11y';

constructor(private announcer: LiveAnnouncer) {}

save() {
// ...save the order...
this.announcer.announce('Order saved', 'polite');
}


Step 6: Check Color Contrast

Text must stand out from its background. For WCAG AA, normal text needs a contrast ratio of at least 4.5:1, and large text and interface parts such as input borders and icons need at least 3:1. Light gray text on white is the most common failure. Do not use color alone to show meaning, such as red for an error, because some users cannot tell the colors apart. Add text or an icon as well.

In Ionic, colors come from CSS variables. When you change a theme color, check the contrast of the text that sits on it:

:root {
--ion-color-primary: #1a5fb4;
--ion-color-primary-contrast: #ffffff;
}

Update the related -rgb, -shade and -tint variables as well, or use Ionic’s color generator so they stay consistent. Browser DevTools show the contrast ratio when you inspect a color.


Step 7: Do Not Block Zoom

Users with low vision zoom the page, or use larger system text. Blocking zoom is a real accessibility failure. Check your index.html, because many starter templates, including some Ionic ones, ship with a viewport tag that disables zooming:

<!-- blocks zoom: avoid -->
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

<!-- better -->
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0" />

Then test your layout at 200% zoom. Text should stay readable, and nothing should be cut off or overlap. Use relative units such as rem for font sizes instead of fixed pixel sizes where you can.


Step 8: Respect Reduced Motion

Some people get dizzy or sick from animations and parallax effects. Operating systems let them ask for less motion, and you can respect that in CSS:

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

Also avoid anything that flashes more than three times per second, and give users a way to pause moving content such as carousels that rotate by themselves.


Step 9: Add Accessibility Linting to Angular

You can catch many template problems while you write code. The angular-eslint package includes an accessibility rule set for templates, which flags things like images without alt, click handlers without keyboard handlers, and invalid ARIA attributes. In a project that uses the older JSON configuration, enable it for HTML templates like this:

{
"files": ["*.html"],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}

Projects that use the newer flat ESLint configuration have a matching preset for the same rules, so check the angular-eslint documentation for your version. Linting is not a full audit, but it keeps the basic mistakes out of your code.


Step 10: Test Automatically and by Hand

Automated tools are fast, and they find only a part of the problems, so use both automated and manual checks. Start with the Lighthouse accessibility audit in Chrome DevTools and the free axe DevTools browser extension. To keep checking on every change, add axe to your end-to-end tests, for example with Playwright:

npm install --save-dev @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('home page has no detectable accessibility violations', async ({ page }) => {
await page.goto('/');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

Then do these manual checks, because tools cannot judge whether labels make sense or whether the experience is pleasant:

  • Keyboard only: unplug your mouse and complete the main tasks of your app.
  • Screen reader: try NVDA on Windows, VoiceOver on macOS and iPhone, and TalkBack on Android. Your Ionic app should be tested on a real device, because it runs in a web view.
  • Zoom: use 200% browser zoom and larger system text.
  • Contrast: check your lightest text and your disabled and error states.


Quick Checklist

AreaWhat to checkAngular or Ionic fix
StructureReal buttons, links, headings and landmarks<button>, routerLink, <main>
Images and iconsAlt text, labels on icon-only buttonsalt, aria-label
FormsLabels, error messages, required fieldslabel, aria-describedby, role="alert"
KeyboardTab order, visible focus, Escape closes dialogs:focus-visible, CDK focus trap
NavigationUnique titles, focus after route changetitle in routes, focus on main
Contrast4.5:1 for text, 3:1 for large text and UI partsIonic color variables, DevTools
ZoomPinch zoom works, 200% zoom does not break layoutFix the viewport meta tag
MotionReduced motion respectedprefers-reduced-motion
TestingAutomated and manual checksLighthouse, axe, screen readers


Best Practices

  • Treat accessibility as part of “done” for every feature, not as a final cleanup task.
  • Add an accessibility statement and a way for users to report problems, and fix the reports.
  • Choose accessible UI components first, and test any custom component you build.
  • Re-test after upgrading Angular, Ionic or your design system, because markup can change.
  • Keep a list of known issues and plan when you will fix them.


Conclusion

Accessible apps are easier for everyone to use, and most of the work is good habits: real HTML elements, labels, visible focus, enough contrast, working zoom and honest testing. Start with the steps in this guide, run an automated check, then try your app with a keyboard and a screen reader. Fix what you find, and keep checking as the app grows.

Related Guides

Post a Comment

0 Comments