Accessibility is one of those topics where the gap between what teams say and what they ship is embarrassingly wide. I have been on both sides of that gap. Early in my career I shipped interfaces that failed basic contrast checks. Over time, leading the redesign at Strongest Families Institute and then shipping accessibility-compliant features at BMO, I made accessibility a non-negotiable part of my own work, in the designs I hand off and in the code I write. This post is the distilled minimum I hold myself to and push for on every team I join.
Why WCAG 2.2 AA Is Your Baseline, Not Your Ceiling
WCAG stands for Web Content Accessibility Guidelines, and the baseline to aim at is level AA of version 2.2. It became a W3C Recommendation in October 2023 and supersedes 2.1, which is now the legacy target. Level A covers the most critical failures. Level AAA is aspirational and often impractical for complex applications. AA sits in the middle: achievable with discipline, meaningful for real users, and legally relevant in Canada. Federally regulated organizations such as banks fall under the Accessible Canada Act and the Canadian Human Rights Act. Ontario adds the AODA, whose accessibility regulation still points at WCAG 2.0 AA, and other provinces have their own statutes. A federally regulated employer operating in Ontario can sit under more than one of these at once. Building to 2.2 AA clears all of them.
If your project was scoped under 2.1, the gap is small and worth closing. WCAG 2.2 adds four AA criteria: 2.4.11 Focus Not Obscured (Minimum), 2.5.7 Dragging Movements, 2.5.8 Target Size (Minimum) at 24x24 CSS pixels, and 3.3.8 Accessible Authentication (Minimum). It also removes 4.1.1 Parsing, which no longer applies. Run a gap review against those five items and you are current.
In banking, AA compliance is not optional. The Accessible Canada Act puts federally regulated organizations under a duty to identify, remove and prevent barriers, so accessibility gets treated as a risk category alongside security and data privacy, and the features I built at BMO had to be accessibility-compliant. That framing is useful anywhere: once you stop pitching accessibility as a moral good and start pitching it as risk mitigation, the conversation with the business gets much shorter.
Color Contrast: The Most Common Failure
WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18pt regular or 14pt bold). These ratios sound technical but the rule is simple: if your grey-on-white body copy looks subtle and elegant, it probably fails.
Two tools worth having: the WebAIM Contrast Checker for anything you can paste a hex pair into, and a contrast-checking plugin inside Figma so the check happens before the handoff. In Figma, I run contrast checks before any design goes to dev. At Strongest Families, I established the design system for the IRIS platform redesign, and contrast was one of the first constraints built into it: every color pair was checked against AA before it became part of the palette, which also kept the palette small. A side benefit that improved consistency across the product.
- Normal text (under 18pt regular / 14pt bold): minimum 4.5:1 ratio
- Large text: minimum 3:1 ratio
- UI components and graphical objects: minimum 3:1 against adjacent colors
- Decorative elements with no informational value: no requirement
- Placeholder text in inputs: must also meet 4.5:1. Often forgotten
- Disabled state elements: explicitly excluded from contrast requirements, but use sparingly
Keyboard Navigation: Every Interactive Element
Every interactive element in your UI must be reachable and operable using only a keyboard. This means links, buttons, form inputs, modals, dropdowns, date pickers, custom select components. All of it. The tab order must be logical and match the visual reading order.
The failure mode I see most often in Angular applications is custom components that swallow keyboard events. A developer builds a beautiful dropdown using divs and click handlers. It works perfectly with a mouse. With a keyboard it is completely invisible. I treat keyboard navigation as part of the acceptance criteria for every interactive component I build, whether or not the ticket spells it out. If it is not in the AC, it does not get tested, and if it does not get tested, it does not get shipped right.
// Bad: div-based button with no keyboard support
// <div class="btn" (click)="submit()">Submit</div>
// Good: native button or explicit keyboard handler
// <button type="button" (click)="submit()">Submit</button>
// If you must use a non-semantic element:
@Component({
selector: 'app-custom-button',
template: `
<div
class="custom-btn"
role="button"
[attr.tabindex]="disabled ? -1 : 0"
[attr.aria-disabled]="disabled"
(click)="handleAction()"
(keydown.enter)="handleAction()"
(keydown.space)="handleAction()">
<ng-content></ng-content>
</div>
`
})
export class CustomButtonComponent {
@Input() disabled = false;
handleAction(): void {
if (!this.disabled) {
// emit action
}
}
}My strong preference is always to use native HTML elements. Button, a, input, select. They come with keyboard support, focus management, and ARIA semantics baked in. Every custom component you build to replace a native one is a debt you will have to pay in ARIA attributes and event handlers.
Focus Management: The Part Everyone Forgets
Keyboard navigation is about reaching elements. Focus management is about where focus goes after an action. This distinction matters enormously in single-page applications.
Consider a modal dialog. A user presses Enter on a trigger button. The modal opens. Where is focus? If you do not manage it, focus stays on the trigger button behind the modal. The user tabs forward and lands on elements in the background that are now visually hidden. This is a failure of SC 2.4.3 Focus Order, which requires focus order to preserve meaning and operability. And it is also a genuinely terrible experience for screen reader users.
- When a modal opens: move focus to the modal container or its first interactive element
- When a modal closes: return focus to the element that triggered it
- When navigating between routes: move focus to the main content heading or the page container
- When showing inline errors after form submission: move focus to the first error or to a summary
- When expanding an accordion: focus can stay on the trigger. Do not move it unless content is dynamically loaded
- Trap focus inside modals: the user should not be able to tab out of an open modal into background content
Before writing any of this yourself, look at @angular/cdk/a11y. It ships cdkTrapFocus, FocusTrapFactory, FocusMonitor and LiveAnnouncer, and on an Angular project you already have the CDK on disk. Hand-roll a trap only when you need behaviour the directive does not cover. Either way, note the empty-list guard below. If the container has nothing focusable, first and last are undefined and calling focus() on them throws.
// Focus trap service for modals in Angular
import { Injectable, ElementRef } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class FocusTrapService {
private focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
getFocusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll<HTMLElement>(this.focusableSelectors)
).filter(el => !el.closest('[hidden]'));
}
trapFocus(container: HTMLElement, event: KeyboardEvent): void {
if (event.key !== 'Tab') {
return;
}
const focusable = this.getFocusableElements(container);
if (focusable.length === 0) {
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}ARIA Labels: Use Them Right or Not at All
ARIA, Accessible Rich Internet Applications, is a set of HTML attributes that communicate semantic meaning to assistive technologies. The most important rule about ARIA is the first rule of ARIA: do not use ARIA if there is a native HTML element that does the job. role='button' on a div is always worse than a button element.
That said, there are legitimate cases where ARIA is necessary. Complex widgets, dynamic content, custom components. Here are the attributes I use most frequently and what each one does.
- aria-label: provides a text label for an element when visible text is absent or insufficient. Use on icon-only buttons
- aria-labelledby: points to the ID of another element whose text serves as the label. Use for form fieldsets and dialogs
- aria-describedby: points to additional descriptive text. Use for input helper text and error messages
- aria-expanded: communicates the open/closed state of accordions, dropdowns, and menus
- aria-live: marks a region that will receive dynamic updates. Screen readers will announce changes automatically
- aria-hidden: hides decorative elements from the accessibility tree. Use on icons inside labeled buttons
- role: overrides the implicit ARIA role. Use sparingly, only when the semantic element is truly not available
// Practical ARIA in Angular templates
// Icon-only button
<button type="button" aria-label="Close notification">
<app-icon name="close" aria-hidden="true"></app-icon>
</button>
// Input with visible label and helper text
<div class="field">
<label id="email-label" for="email">Email address</label>
<input
id="email"
type="email"
aria-labelledby="email-label"
aria-describedby="email-hint email-error"
[attr.aria-invalid]="emailControl.invalid && emailControl.touched" />
<span id="email-hint">We will never share your email.</span>
<span id="email-error" role="alert" *ngIf="emailControl.invalid && emailControl.touched">
Please enter a valid email address.
</span>
</div>
// Expandable panel
<button
[id]="'btn-' + id"
[attr.aria-expanded]="isOpen"
[attr.aria-controls]="'panel-' + id"
(click)="toggle()">
{{ title }}
</button>
<div
[id]="'panel-' + id"
[hidden]="!isOpen"
role="region"
[attr.aria-labelledby]="'btn-' + id">
<ng-content></ng-content>
</div>Screen Reader Testing: You Cannot Skip This Step
Automated tools like Axe, Lighthouse, and IBM Equal Access Checker catch a lot, but not everything. Deque's 2021 study of its own audits found that automated axe testing caught around 57% of issues by volume (the older 20 to 30% figure refers to coverage of WCAG success criteria, not issue volume). The remainder requires manual testing. The most valuable manual test is screen reader testing. Actually turning off your monitor and trying to complete a task using only audio.
My testing setup uses NVDA on Windows with Firefox and VoiceOver on macOS with Safari. Both are among the five most common pairings in WebAIM's latest published screen reader survey, although NVDA is now used with Chrome about twice as often as with Firefox. Before a feature of mine ships, I do a screen reader walkthrough. It is not a full audit. But it catches the obvious failures before they reach production.
- NVDA + Firefox (Windows): free, widely used, essential for Windows-heavy enterprise audiences
- VoiceOver + Safari (macOS/iOS): built in to Apple devices, critical for mobile
- TalkBack + Chrome (Android): essential for Android mobile testing
- JAWS + Chrome or Edge (Windows): expensive enterprise screen reader common in banking and government
A typical example: a multi-step form shows 'Step 1 of 5' visually, but VoiceOver reads only 'Form.' The progress indicator is built entirely with CSS classes, with no semantic markup. Putting an aria-label on a plain container does not fix it, because screen readers do not reliably announce labels on generic elements. What works is visually hidden text that announces the current step, plus aria-current='step' on the active item of the step list. That is the kind of issue no automated tool will catch.
What I Check at Each Stage
I check accessibility at three stages: design review before dev handoff, component review during development, and a pre-release pass. At Strongest Families I owned all three, because I was designing and handing off. At BMO I was on the development side, working with the bank's design system, which another team owned, so the stages I applied were the component review and the pre-release pass on my own features. The design-stage checklist covers the following.
- All text and interactive element contrast ratios checked and documented in Figma
- Focus states visible and styled for every interactive element. Not just the default browser outline
- Error states include both color and an icon or text. Never color alone
- Form inputs have visible labels. Placeholder text alone fails SC 3.3.2 Labels or Instructions, and SC 1.3.1 as well when there is no programmatic label at all
- Any content conveyed by color alone has a secondary indicator
- Touch targets meet SC 2.5.8 Target Size (Minimum), 24x24 CSS pixels, which is the AA rule. On mobile I design to 44x44, which is SC 2.5.5 at AAA and matches the Apple and Material platform guidance
- Modals and dialogs have an explicit close mechanism and a heading
- Images have alt text specified in the design. Decorative images are explicitly marked as decorative
- Data tables have column headers and row headers identified
- Motion and animation have a reduced-motion consideration
The Business Case Is Easier Than You Think
Every time I have had to make the case for accessibility investment I have used the same three arguments. First, legal risk: in Canada and most western jurisdictions, failing to provide accessible digital services to a bank's customers is actionable. Second, market size: about 16% of the world population, 1.3 billion people or 1 in 6, live with a significant disability according to the WHO, and accessible design improves usability for all users, not just those with disabilities. Third, the curb-cut effect: features built for accessibility, like captions, voice control, and keyboard shortcuts, are used heavily by people without disabilities.
Accessibility is not a feature you add at the end. It is a quality attribute you build in from the start, like performance or security. If you are retrofitting it, you are already paying the penalty.
The minimum you need to know is not actually that much. Learn the four WCAG principles: perceivable, operable, understandable, robust. Run your designs through a contrast checker before handoff. Test with a keyboard before every release. Test with a screen reader before every major feature launch. Add ARIA only where native HTML is insufficient. That is the minimum. Start there and build upward.
References
- WCAG 2.2. https://www.w3.org/TR/WCAG22/
- WebAIM Contrast Checker. https://webaim.org/resources/contrastchecker/
- NVDA screen reader. https://www.nvaccess.org/download/
- Angular CDK a11y module. https://material.angular.dev/cdk/a11y
- AODA (Ontario). https://www.ontario.ca/laws/statute/05a11
- Accessible Canada Act (S.C. 2019, c. 10). https://laws-lois.justice.gc.ca/eng/acts/A-0.6/
- World Health Organization. Disability fact sheet. https://www.who.int/news-room/fact-sheets/detail/disability-and-health
- WCAG 2.2 Understanding SC 2.5.8 Target Size (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html