by Tyler Kleeberger  in Angular / Accessibility

Designing Human-Oriented Software: How Angular’s Accessibility Features Can Enhance the User Experience

From ARIA to WGAC’s accessibility standards, any business can elevate the user experience (UX) of its software to enhance their product value. From responsive design and semantic HTML to keyboard navigation, color themes, and accessibility in forms, your company’s software, especially when using the Angular framework, can unlock valuable potential.

There are few worse feelings than experiencing a world not built for you. 

For much of my life, I have had the privilege of being connected to the deaf community. My sister, being born deaf, introduced my family and me to a world we had never truly considered before. While we discovered profound beauty and learning, I was constantly struck by how often everyday experiences—things I took for granted as a hearing person—posed ceaseless difficulty, frustration, and uncertainty for a deaf person. Time and again, I realized that the norms of our society—our habits, tools, and systems—were built with someone like me in mind, not my sister.

These experiences are not unique to those with sensory impairments. Standardized testing, for example, is often criticized for a rigid format that disproportionately disadvantages students with neurodivergence, dyslexia, or other learning differences—so much so that their scores may not accurately reflect their intelligence. Outdoor spaces, intended as sanctuaries for nature’s beauty, are frequently inaccessible to those with physical disabilities. Medical research bias has led to treatments tested primarily on men, sometimes resulting in significantly worse side effects for women. Even seemingly trivial design choices—like wireless headset microphones assuming the wearer will have a belt, or scissors being designed primarily for right-handed users—reveal a broader pattern: we often design our world without considering everyone, unintentionally sending the message that it wasn’t built for them.

For those of us in various fields of technology, this may be a more pressing issue. Because the world is increasingly becoming a digital one, and we are the primary builders of the daily experiences of life. The proportion of time an individual spends using software is high and constantly increasing. Life and digital landscapes are practically synonymous. In the ubiquitousness of technology, we must at least realize that we are building the tools, habits, and systems of the human experience.

At the least, developers and companies, therefore, must be aware of how we are building these things.

We ought to be aware that how we design the modern world’s tools implicates who can use them, how they can use them, and what they will experience when they use them. 

Accessibility === UX

The technology sector is not lacking in expertise, tools, and opinions on the topic of accessibility. This is not a new venture or a novel idea. Alas, we don’t need more articles making a case for accessibility or outlining its concepts. However, within the breadth of this conversation, we do need to keep the conversation going. We also need to focus on how we move this conversation from our heads into our hands. It wouldn’t hurt to also ask deeper questions about how this theme ought to impact our perception and motivation when writing code or building products. 

That’s because accessibility is actually a UX question. Fortunately or unfortunately, “Accessibility” has taken on its own connotation; occasionally with political or ideological overtures. Some circles even use it as a badge of distinction as to what side of the cultural coin one is on. Yet, putting aside the philosophical ethics (though it is a conversation worth continuing in its own form), the term accessibility is simply a description of a user’s experience. Accessibility is not a question of belief, it is a question of UX.

The concern is not whether or not your app is accessible. It is, rather, how your app is accessible, to whom it is accessible, and whether or not the access individuals have produces a good experience or a bad one. 

Considerations for Accessibility

That concern, when honest, is complicated. Overwhelming, even. With over eight billion people, as the cliche goes, you can’t make everyone happy. Nor can you consider every single physical, mental, emotional, or existential context. Is it even possible to build software that accounts for every culture, perspective, way of life, situation, or circumstance? Not to mention that the way someone is feeling that day might have a bigger impact on their experience of your software than any ARIA attribute you use. 

Yet, the considerations should still be, well, considered. I suppose it is worth acknowledging that absolute “accessibility” to every and all circumstances, while a noble ideal, is not practically obtainable. But we can still try. And we can still emphasize the most ubiquitous circumstances. 

In this regard, the statistics usually range somewhere around 15% of the population having some sort of impairment that can affect their technological experience. There are certainly the categories that have received notable and necessary voice as of late, such as the blind and visually impaired community. Simply the existence of assistive technologies like screen readers is a massive step in building a digital world that is more accessible.  But there are others which are proportionally common and worth mentioning. Users experiencing our applications could be those with:

  • Learning impairments
  • Cognitive or behavioral impairments
  • Deafness or hearing impairments
  • Speech impairments
  • Physical impairments and injuries

These are humans who might not have the same experience of the world but who should nonetheless be able to experience our tools. This list, while mostly focusing on sensory issues, can continue and include those with neurodivergence or situational impairments. What about someone who has broken their arm? Does that affect their ability to access and use what we are building? While these examples should be elevated in importance because of their temporal and physical implications, I tend to also wonder about other human qualities and experiences that we may want to consider in terms of accessibility. For example, how should age or technological familiarity impact our design? Boomer jokes and cultural tropes aside, there is a large percentage of the population who feels increasingly unfamiliar with the complexities of our tools yet who still uses and even depends on them.

From racial, ethnic, or linguistic differences to mental and emotional experiences and even the cultural, ideological, political lenses people carry with them into our applications – the diversity of circumstances reflected in the human species,  especially in a globalized world, all should at least be considered because, in the end, these are humans who might use, and possibly even benefit from, the technological world we are building.

The goal, however, is to remove barriers to the greatest degree possible. While we cannot control the ultimate success of that endeavor, we can control the attempt and the consideration. This is something that the accessibility movement has articulated well – that the intention is simply to allow human beings to fully experience and interact with digital products and services. 

Considerations for Your Business

Designing applications that work well – especially for those with impairments (whether permanent or temporary), differences, or who are relying on assistive technologies – is ultimately a business motivation. Again, the ethical conversation is important, but we should not pretend there aren’t economic motivations for a business, as well. Yes, we should make a better user experience for those using our applications because it is good for the human condition, however, removing the friction and barriers from accessing our content or using our technology is not just about the user as a consumer, but also that the user is a customer. In most businesses, there is often a goal to ensure that those users stay as customers, too. 

Designing and developing with accessibility in mind means more people have access to your product. Not doing so is a risk. It is a risk in eliminating users, but is also a risk of reputational harm. There is no greater threat to your business than a group who has taken up advocacy against your product 

In contrast, how would someone like my sister respond to a digital product or software application that intentionally considered her difference in how that tool was built? Would a sense of solidarity motivate her support further than a purchase? Would it lead to providential sharing amongst those in her community? Even more than being viable for a company, is such an act better for the technological sector as a whole and possibly even humanity itself?

Sure, we are often bombarded by flippant yet shallow gestures at this kind of solidarity. Often, the rhetorical acrobatics actually isolate the communities that companies try to market to with nice sentiments which, instead, come across as patronizing. However, when the actual user experience is enhanced in tangible ways, the wins are shared by company and consumer alike. 

When the goals of the company or product meet the goals of the user, it realizes better experiences; which realize better businesses; which realize a better world..

However, beyond the potential increase in user base and the potential solidarity generated by making your product accessible, you’re also going to avoid some potential pitfalls. While not required by law (except for products issued by the government), lawsuits are not unheard of. Websites are increasingly becoming considered as a service of public accommodation – just like a park or restaurant. Accessibility in this regard is covered by the ADA. Avoiding reputational damage is a worthy motivation, but so is avoiding lawsuits and fines. Even if there is no moral imperative, it may be worth the social and legal ones. 

But again, this is a question of UX. This is about improving the user experience; about making a product for people that should function in the best interests of those people. Consider any market sector that deals with engineering, building, or designing. What is being made does not exist in a vacuum. We build in real places for real people, and the context of the consumer should determine the value of the product. At the end of the day, maybe software should be considered through the lens of the hospitality industry – how should someone experience your product?

That is what the standards, practices, and guidelines of accessibility are attempting to provide.

Accessibility Guidelines & Practices

The Web Content Accessibility Guidelines (WGAC) provide the foundation for accessibility best practice with their thorough and applicable standards. These principles should guide software development alongside the architecture of the code itself. In essence, all software should be:

  1. Perceivable – able to be seen and engaged with clarity. The user should be aware of the content and be able to comprehend it.
  2. Operable – the software should function smoothly without disruption ensuring simplicity and usability. Excess functionality that could impede users with limitations should be removed.
  3. Understandable – written and visual content should be cognitively, linguistically, and culturally understandable to the user.
  4. Robust – the dynamics of the code should be easily interpreted, especially by those using assistive technology.

As guiding principles, these are helpful. However, how does this get implemented in an actual project? First, let’s look at some specific guidelines and topics to consider as a sort of accessibility checklist. Then we’ll explore some practices and examples. 

Guidelines for Accessibility

Responsive Design

This topic is an entire perspective in itself, but the principles of responsive design are synchronous with accessibility – especially in terms of making an application “accessible” to a variety of contexts. Responsive design naturally removes limitations based on devices, screen sizes, and user needs. This includes the ability to scale text or use high contrast modes. This adaptability makes an application usable across a range of environments, devices, and needs. 

Some specific implementations to consider are:

  • Scalable Text: Ensure that text can be resized up to 200% without breaking the layout or functionality. This allows users to adjust the text size according to their needs without negatively impacting the usability of the app.
  • High Contrast Mode: Provide clear visual contrast for text and elements, especially when users activate high contrast modes in their operating systems or browsers. For instance, offering an example where the background and text colors switch to provide a higher contrast could help users with visual impairments navigate your site more easily. Using advanced CSS, an app can also be responsive to a high contrast mode setting similar to how dark and light modes adjust themes. 
  • Color Contrast: It’s essential that text contrast meets the WCAG standard of 4.5:1 for normal text and 3:1 for large text. Anywhere color is used, it should meet the criteria for contrast levels. This can be checked with a contrast checker. Further, colors should never be the sole means of conveying information—consider users who are colorblind or those who cannot distinguish between certain color schemes. Relying on red font for an error and green font for a success with no other indicators can cause a bit of confusion if the user does not differentiate between red and green. Instead, always include actual messaging via text, icons, or general patterns for how you convey information.

User Interaction Responses

User interaction responses refer to how your application communicates with the user, particularly when something goes wrong. It’s essential to provide clear and actionable feedback for errors, failures, or issues during interactions.

Additionally, users should always know their current location in the app with navigation insights and why they are there if something unexpected occurs (e.g., a form error, failed submission, or unexpected behavior). For example, if a user thought they uploaded a file but it failed and redirected them away from an interface, providing error messages with specific instructions on how to resolve the issue or what next steps can be taken will make a significant difference in the user experience.

Legible and Accessible Content

Content must be clear, concise, and cognitively accessible to users, ensuring everyone can understand and engage with it.

  • Clear Language: Use simple, direct language whenever possible. Avoid jargon or overly complex phrases. The goal is to make content that can be easily understood by the broadest possible audience, including those with cognitive impairments.
  • Readability: Ensure text is legible by choosing appropriate fonts, sizes, and spacing. The layout should not overwhelm the user, and content should be structured logically.

Clear Navigation With Logical Flow and Structure

Effective navigation and a clear structure help users find what they need without frustration, creating a logical flow throughout the application. As software becomes more and more common in the daily life of the majority of people, this is increasingly important. There is a sort of empathic practice to considering what a new or inexperienced user needs to make sense of your app. You have to leave behind your mind with all of its insights and insider knowledge and inhabit the perspective of the vast array of people who may compose your audience. How many bad product reviews are a result of unclear or uncertain processes for working the product? 

The goal here is intuitiveness. And not just intuitive to you – the person who envisioned and designed the thing – but to those who may be the least inclined to perceive what exactly they are supposed to do when the app first opens. A couple of key emphases should be:

  • Navigation: Include clear and descriptive page titles, meaningful links, and keyboard focus indicators. Proper headings (H1, H2, etc.) allow users to quickly navigate through the content and understand the context of each section.
  • Logical Flow: Organize content in a way that makes sense. Group related items together and ensure that users can intuitively navigate through the app, finding information in a manner that feels natural and fluid.
  • Instructive – without bombarding a page with words, how can implicit instructions be utilized while providing explicit instructions when necessary? This could be more descriptive labels on buttons or simply refraining from using ambiguous icons. Tooltips can also be helpful in certain situations so as to not plague the design with words but still offer a way for secondary instructions to users to ensure they are tracking with the intended flow of the product.

Media

Providing media with accessible alternatives ensures users can interact with audio and visual content in a way that works for them.

  • Alt Text: Provide descriptive alt text for all images and other media, if applicable. This ensures users who are blind or visually impaired can understand the context and content of images via screen readers. This also accounts for users who may not have adequate network connectivity.
  • Transcriptions and Captions: Offer transcriptions for audio and captions for video content. This is vital for users with hearing impairments or those who prefer reading over listening.
  • Clarity: Ensure that media content is easy to see and hear. Allow users to adjust audio levels and offer dynamic options for users to customize their media experience according to their needs.

ARIA (Accessible Rich Internet Applications)

ARIA is designed to enhance accessibility for dynamic content and advanced user interface controls that are not typically accessible through standard HTML.

  • Purpose of ARIA: ARIA provides additional information to assistive technologies, making applications more usable for people with disabilities. This is especially important for complex, dynamic interfaces or content that changes frequently.
  • The Accessibility Tree: ARIA allows developers to expose parts of the DOM (Document Object Model) to assistive technologies by modifying, exposing, or augmenting the Accessibility Tree, which is created by the browser. This tree represents all markup elements, attributes, and text nodes, ensuring that assistive technologies can understand and navigate the application as intended. ARIA does not affect the visual appearance of elements but provides semantic meaning where it may be lacking. While designing templates is typically considered from a purely visual level, the accessibility tree with ARIA emphasizes the structural flow of content.

Time Constraints

Time-based interactions in an application can be challenging for some users, especially those with impairments that affect their speed of interaction and processing. This also concerns users with slow network accessibility.

  • Avoid Time Constraints: Whenever possible, avoid time-limited actions. For actions where time constraints are necessary, ensure there are mechanisms to pause, extend, or adjust the time limits based on the user’s needs.
  • Flexible Timing: Provide users with the ability to delay or extend time-based interactions, ensuring they are not penalized for slower responses or unforeseen circumstances.

Semantic HTML

Semantic HTML is crucial for ensuring your application’s content is understandable by both humans and assistive technologies and has corresponding effects to the goals of ARIA and the accessibility tree.

  • Screen Reader Compatibility: Use semantic HTML to ensure content is accurately interpreted by screen readers. Correct use of ARIA roles and attributes is essential for conveying meaning to users who rely on assistive technologies and semantic HTML assists in accomplishing this goal.
  • Adaptable Content: Build your HTML structure to be flexible and adaptable. Ensure that content remains accessible even if styling or JavaScript is removed (for example, when a user disables CSS or scripts).
  • Landmark Elements: Use semantic HTML tags (e.g., <header>, <nav>, <main>, <footer>) as they are natively supported by browsers and come with implicit ARIA roles. These tags help users with screen readers or other assistive technologies understand the layout and structure of the page without the need for additional ARIA roles.

Visual Stimuli

Content that blinks, flashes, or moves too quickly can be difficult or even harmful for some users. Avoid content that blinks or flashes more than three times in any one second period, as this can trigger seizures in users with photosensitive epilepsy. Provide mechanisms to stop or pause such content where necessary.

Practices & Examples for Accessibility

Using ARIA Tools

ARIA (Accessible Rich Internet Applications) is a powerful tool for enhancing the accessibility of web content, especially dynamic content and complex user interfaces. It provides additional information about the roles, properties, and states of elements, making them more understandable for users of assistive technologies.

Three Key Features and How to Use Them

ARIA provides three key features that developers can use to enhance accessibility:

  1. Roles: These define the type or purpose of an element (e.g., role="button“).
  2. Properties: These describe the characteristics of an element (e.g., aria-describedby="nav-link").
  3. States and Values: These provide information about the current state or conditions of an element (e.g., aria-pressed="false").

As we’ll see, this is a very simple example just to show the features and is not the recommended best practices.

Using ARIA Effectively

  • Use ARIA only when necessary: Most HTML elements are already accessible. For instance, there is no need to add role="button" to a <button> element. This is why we should use  semantic HTML wherever possible. Which means instead of using a div, we should have just used a button!
<!-- Correct --> 
<button aria-label="Save document">Save</button> 

<!-- Incorrect --> 
<a role="button">Save</a>
  • Ensure Keyboard Accessibility: Interactive ARIA elements should always be keyboard accessible. For example, use tabindex="0" for custom elements that need focus. We’ll explore this further in the next section.
  • Avoid Hiding Focusable Elements: Don’t use aria-hidden="true" or role="presentation" on elements that are focusable. These properties tell assistive technologies to ignore the element, making it inaccessible.
  • Accessible Names for Interactive Elements: Ensure every interactive element has an accessible name, which can be achieved using aria-label, aria-labelledby, or aria-describedby.
  • When Not to Use ARIA: ARIA is only required when an HTML element does not provide inherent accessibility. Overusing ARIA can lead to complications, so always prefer semantic HTML elements before considering ARIA.

Keyboard Navigation & Focus

Keyboard navigation is essential for ensuring that users can navigate your application using only the keyboard. All interactive elements, such as links, buttons, and forms, should be fully operable via the keyboard, with visual indicators of focus. Ideally, your app can be completely operated with just a keyboard.

Focus refers to the element on the screen that is currently receiving input from the keyboard. The tab order determines the focus order.

When users navigate with a keyboard, it is crucial to maintain clear focus on interactive elements. This is particularly important for users with physical limitations (e.g., injury or fine motor skill impairments) or vision issues who rely on keyboards, magnification software, or screen reader software. Different key shortcuts should be available and functional depending on the assistive technology in use.

Logical Tab Order

The tabbing sequence should follow a logical and intuitive order, typically matching the visual flow of the page. This ensures that users can navigate from top to bottom and left to right as they would read a page (with considerations for users of languages that read right to left. As a general note, having your app reflect its content flow based on the selected language of the user is certainly a good idea).

The tabindex attribute is used to control the focus order. A tabindex of 0 indicates that an element is focusable in the natural order, while positive tabindex values (e.g., tabindex="1") specify the order. Negative tabindex values remove elements from the focus order which is advisable when necessary. Using positive tabindex values are usually cautioned against except for rare cases. 

Having multiple elements with a tabindex=”0” will navigate and focus in order of appearance in the DOM. Many HTML elements are natively focusable so setting a tabindex=”0” is usually reserved for elements without that default behavior that you want to include in the keyboard navigation. 

Making a normally focusable element inactive (such as a button that is inoperative until an input field is filled in) is an example of using a negative tabIndex. Again, this should be very intentional and not used to hack design or functionality. If you want assistive technology or keyboard navigation to specifically not include an element, then it is worth setting a negative tabindex.

Focus Indicators and Styling

A visible focus indicator is crucial to help users identify where they are on the page. Browsers provide default focus styles, but you can customize these to match your application’s theme.

For a customized focus indicator, use outline, border, or background changes to make the focus indicator more visible. Ensure that the indicator is not hidden beneath other components.

Avoid setting outline: none; as it removes the focus indicator, which is vital for keyboard navigation.

Example: Custom Focus Indicator

a:focus {

  outline: auto 5px Highlight; /* Non-webkit browsers */

  outline: auto 5px -webkit-focus-ring-color; /* Webkit browsers */

}

Routing & Focus Management in Angular

When navigating between pages or content sections in a single-page application (SPA), it is important to update the focus to help users keep track of their position.

Focus on Content After Routing

In Angular, you can use the NavigationEnd event from the Router service to update the focus when a new page or content section is loaded. This helps users immediately know where they are, especially when navigating through dynamic content.

In this example, the focus is automatically set to the main content header after navigation.

Example: Focus Management with Angular Router
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';


@Component({
  selector: 'app-my-component',
  templateUrl: './my-component.component.html',
  styleUrls: ['./my-component.component.css']
})
export class MyComponent implements OnInit {
  private readonly router = inject(Router);
  private readonly document = inject(DOCUMENT);


  ngOnInit(): void {
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe(() => {
      const mainHeader = this.document.querySelector('#main-content-header');
      if (mainHeader) {
        (mainHeader as HTMLElement).focus(); // Ensure focus is set properly
      }
    });
  }
}

To help users identify the current page, you can apply CSS classes or use aria-current for links that represent the active page.

<nav>
  <a routerLink="home"
     routerLinkActive="active-page"
     [routerLinkActiveOptions]="{ exact: true }"
     [attr.aria-current]="activeRoute === 'home' ? 'page' : null">
    Home
  </a>
  <a routerLink="about"
     routerLinkActive="active-page"
     [routerLinkActiveOptions]="{ exact: true }"
     [attr.aria-current]="activeRoute === 'about' ? 'page' : null">
    About
  </a>
  <a routerLink="contact"
     routerLinkActive="active-page"
     [routerLinkActiveOptions]="{ exact: true }"
     [attr.aria-current]="activeRoute === 'contact' ? 'page' : null">
    Contact
  </a>
</nav>

<na

Using RouterLinkActive alongside the aria-current attribute ensures that users can identify the currently active page. However, RouterLinkActive alone does not set aria-current=”page”, so we need to bind it dynamically. 

To achieve this, we track the active route in the component and use Angular’s property binding ([attr.aria-current]) to apply aria-current="page" only to the currently active link. This ensures that screen readers correctly announce the active page and prevents multiple links from appearing as active at the same time.

Additionally, we use [routerLinkActiveOptions]="{ exact: true }" to ensure that the active class is applied only when the route matches exactly. This prevents links from being marked as active for partial matches (e.g., /about should not activate for /about/team).

By storing the active route as a reactive signal in the component and updating it on NavigationEnd events, we optimize performance while maintaining accessibility best practices.

An example of a NavigationComponent class could look something like this:

import { Component, inject, signal, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.css']
})
export class NavigationComponent implements OnInit {
  private readonly router = inject(Router);
  activeRoute = signal<string>('');

  ngOnInit(): void {
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      this.activeRoute.set(event.urlAfterRedirects.replace('/', ''));
    });
  }
}

There are more emphatic examples of obtaining accessibility that also correlate with the values of Angular. Beyond these overarching and thematic examples and guidelines, it is worth perusing some of the most necessary treatments for accessibility and some of the most unique advantages of the Angular framework.

Angular’s Accessibility Advantages

Many cases can be made for the value of the Angular framework when building web applications; especially for complex applications that need to scale. Adding an emphasis on accessibility into the conversation strengthens the value Angular provides. 

The Angular CDK (Component Development Kit) offers a range of mechanisms (which works exceptionally well with Angular Material’s suite of UI components)  designed with accessibility in mind. This includes the inclusion of the a11y package that provides additional tools to enhance accessibility features. All of this is made most evident in the creation of a form.

An Accessible Angular Form

Forms are the most evident feature of an application where accessibility can enhance or hinder your app as this is the most user-dependent experience in any software. If you’re using a form and the user can’t engage with it correctly, you’ve rendered that aspect of your app and its intended role completely useless. 

The good news is that making a form comprehensively accessible requires little extra work. Meanwhile, the value it brings is useful to every user, not just those with specific limitations. 

Here’s how an accessible form might look using the Angular framework with some additional assistance from a11y and the Angular CDK. To run this example, you’ll need to set up an Angular project in your preferred IDE and ensure that you’ve installed Angular Material (ng add @angular/material).

We are going to use Angular Material for this example, but it is not necessary. It is worth noting that Ionic (another popular styling library) has useful accessibility features, as well. However, Angular Material is much more emphatic in this regard. 

The Angular Material components automatically come with built-in accessibility features, such as proper keyboard navigation and focus management. The goal here is to ensure every form control has clear, accessible labels, validation feedback, and proper focus management.

View the full example on StackBlitz.

Accessibility Enhancements in Our Angular Form Example

Semantic HTML Elements

We use semantic HTML elements like <form> and <fieldset> to provide structural information to assistive technologies. The <fieldset> element groups related form controls, which helps screen reader users understand the form’s organization.

<form [formGroup]="form" (ngSubmit)="onSubmit()" novalidate>
  <fieldset>
    <!-- Form fields here -->
  </fieldset>
</form>

 

Informative Labels

Each input field has a descriptive <mat-label> that clearly communicates its purpose. The label includes “(required)” to visually indicate mandatory fields. Angular Material automatically associates these labels with their corresponding inputs, ensuring screen readers announce them properly.

 <mat-form-field appearance="outline">
   <mat-label>Full Name (required)</mat-label>
      <input
        matInput
        id="name"
        formControlName="name"
        required
        aria-required="true"
      />
    </mat-form-field>

Required Field Indicators

We use both the standard HTML required attribute and the ARIA aria-required=”true” attribute to ensure all users, including those using assistive technologies, are aware that these fields must be completed.

<input matInput id="name" formControlName="name" required aria-required="true">

Error Messaging

Using Angular 17’s @if control flow syntax, we conditionally display error messages only when needed. The mat-error component provides clear visual styling, and we’ve added aria-live=”assertive” so screen readers announce these errors immediately when they appear.

@if (form.controls.name.invalid && form.controls.name.touched) {
  <mat-error aria-live="assertive">Name is required.</mat-error>
 }

Additional Context With Visually Hidden Text

The .visually-hidden class provides additional instructions that are only announced by screen readers, not displayed visually. Unlike using display: none, this approach ensures the content remains accessible to assistive technologies while being invisible on screen.

<span id="email-validation" class="validation-message visually-hidden">
  Please use a valid email address format.
</span>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap; 
  border: 0;
}

Input Descriptions with ARIA

The aria-describedby attribute connects the input to its description (the validation instructions) and could be used here to allow screen readers to announce this additional context when users focus on the email field.

<input matInput id="email" formControlName="email" type="email" required aria-required="true" aria-describedby="email-validation">

Keyboard Focus Styles

Using the :focus-visible pseudo-class (which targets elements focused via keyboard navigation), we provide highly visible focus indicators that help keyboard users track their position in the form without affecting mouse users.

input:focus-visible, button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

Keyboard Navigation & Logical DOM Order

The form controls are arranged in a logical top-to-bottom sequence in the DOM, ensuring that the tab order makes sense for keyboard users. We’ve avoided using CSS that might visually reorder elements while preserving this logical tab sequence.

<mat-form-field><!-- Name field --></mat-form-field>
<mat-form-field><!-- Email field --></mat-form-field>
<button><!-- Submit button --></button>

Accessible Button States

The submit button is:

  • Automatically disabled when the form is invalid
  • Uses both the HTML disabled attribute and ARIA’s aria-disabled for maximum compatibility
  • Has visual styling that clearly indicates when it’s disabled
  • Uses aria-live=”polite” so state changes are announced by screen readers
   <!-- Submit Button -->
    <button
      mat-raised-button
      color="primary"
      [disabled]="form.invalid"
      type="submit"
      aria-live="polite"
      [attr.aria-disabled]="form.invalid ? 'true' : 'false'"
    >
      Submit
    </button>
button[disabled] {
  opacity: 0.7;
}

Reactive Form Validation

Angular’s reactive forms provide built-in validation capabilities. We use Validators.required and Validators.email to ensure data integrity, which works seamlessly with our accessibility features to provide meaningful feedback.

 form = new FormGroup({
    name: new FormControl<string>('', {
      nonNullable: true,
      validators: [Validators.required],
    }),
    email: new FormControl<string>('', {
      nonNullable: true,
      validators: [Validators.required, Validators.email],
    }),
  });

Using LiveAnnoucer for Screen Readers

The LiveAnnouncer service from Angular CDK announces important status changes to screen reader users. We use the ‘assertive’ priority to ensure immediate announcement of form submission results.

Additionally, to improve accessibility further, we could use aria-live=”polite” on the submit button to provide status updates when the form is either successful or contains errors. Polite will not interrupt any current screen reader communication.

 onSubmit() {
    if (this.form.valid) {
      this.liveAnnouncer.announce('Form submitted successfully', 'assertive');
      console.log('Form Data:', this.form.value);
    } else {
      this.liveAnnouncer.announce(
        'There are errors in the form. Please correct them.',
        'assertive'
      );
      this.focusFirstInvalidControl();
    }
  }

Programmatic Focus Management

When form submission fails due to validation errors, we programmatically move focus to the first invalid field. This helps all users, especially those using screen readers or keyboards, quickly locate and fix errors without having to search through the form.

focusFirstInvalidControl() {
    const firstInvalidControl = Object.keys(this.form.controls).find(
      (key) =>
        this.form.controls[key as keyof typeof this.form.controls].invalid
    );
    if (firstInvalidControl) {
      const element = this.document.getElementById(firstInvalidControl);
      element?.focus();
    }
  }

Implementing Keyboard Navigation and Focus Management

Using FocusMonitor to Track Focus Origins

FocusMonitor is a powerful service that helps us track how an element gains focus. It not only captures focus and blur events, but also tracks the origin of the focus (keyboard, mouse, touch, or programmatic focus). This allows us to create a more inclusive and predictable experience for keyboard and screen reader users.

A Note on NgZone With FocusMonitor

Most Angular projects don’t require direct interaction with NgZone, as Angular’s built-in change detection typically handles updates when an event occurs within Angular’s zone. However, FocusMonitor operates outside of Angular’s zone, as it listens for native DOM events such as focus and blur triggered by user interaction (keyboard, mouse, etc.). Since these events happen outside of Angular’s default change detection cycle, Angular might not detect these updates unless we explicitly tell it to.

NgZone is a service in Angular that allows you to run code inside Angular’s zone, ensuring that Angular can track changes and update the view accordingly. In our case, when focus changes on an element, FocusMonitor emits an observable with the new focus origin (mouse, keyboard, touch, etc.), and we need to run the update inside Angular’s zone using this._ngZone.run() to trigger change detection. This ensures that the UI reflects the current focus state, keeping it in sync with Angular’s view model.

Without using NgZone, Angular wouldn’t automatically know that the component’s state has changed, so the UI wouldn’t be updated. This could lead to inconsistent behavior, where the template shows outdated information about the focus state of elements.

Example HTML Template

<div class="focus-monitor-container">
  <h2>Focus Monitoring Example</h2>

  <!-- Buttons monitored for focus origin -->
  <div class="focus-monitor-section">
<button #button1 class="focus-monitored-element" aria-describedby="button1-status">

      Button 1 ({{button1Origin}})
    </button>
<span id="button1-status" class="visually-hidden">{{button1Origin}}</span>
    <button #button2 class="focus-monitored-element" aria-describedby="button2-status">
      Button 2 ({{button2Origin}})
    </button>
 <span id="button2-status" class="visually-hidden">{{button2Origin}}</span>

  </div>

  <!-- Subtree monitored for focus origin (including input, textarea, buttons) -->
  <div class="focus-monitor-section">
    <div #subtree>
      <p>Focus monitored on subtree ({{subtreeOrigin}})</p>
      <input type="text" placeholder="Focus me!" />
      <textarea placeholder="Focus me too!"></textarea>
      <button>Subtree Button 1</button>
      <button>Subtree Button 2</button>
    </div>
  </div>

  <!-- Buttons to programmatically focus elements -->
  <div class="focus-monitor-section">
    <h3>Interactive Area</h3>
    <div>
      <button (click)="simulateFocus('button1')">Focus Button 1 Programmatically</button>
      <button (click)="simulateFocus('button2')">Focus Button 2 Programmatically</button>
      <button (click)="clearFocus()">Clear Focus</button>
    </div>
  </div>
</div>

  1. #button1, #button2, and #subtree:
    • These are Angular template references that allow us to reference DOM elements directly in our TypeScript code. In the FocusMonitor example, we use these references to start monitoring focus on the respective elements.
  2. Focus Monitoring:
    • The buttons (button1 and button2) are individual elements whose focus origin will be tracked. The <div #subtree> contains multiple focusable elements, and this entire section (subtree) will also be monitored.
  3. Dynamic Binding with {{button1Origin}}:
    • The button1Origin and button2Origin values are dynamically updated in the template as they are bound to the component’s properties. These properties will store the focus origin (e.g., keyboard, mouse, program, or blurred), allowing you to display the current state.
  4. Interactive Area:
    • This section has buttons for programmatically focusing the elements. This is a way to simulate focus changes and test focus behavior without requiring user input. The simulateFocus and clearFocus methods are triggered on button click.

Example Component Class

import {
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  ElementRef,
  NgZone,
  OnDestroy,
  ViewChild,
  inject,
} from '@angular/core';
import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

@Component({
  selector: 'focus-monitor-advanced-example',
  templateUrl: './focus-monitor-advanced-example.html',
  styleUrls: ['./focus-monitor-advanced-example.css'],
})
export class FocusMonitorExample implements OnDestroy, AfterViewInit {
  private _focusMonitor = inject(FocusMonitor);
  private _cdr = inject(ChangeDetectorRef);
  private _ngZone = inject(NgZone);

  @ViewChild('button1') button1: ElementRef<HTMLElement>;
  @ViewChild('button2') button2: ElementRef<HTMLElement>;
  @ViewChild('subtree') subtree: ElementRef<HTMLElement>;

  button1Origin: string = this.formatOrigin(null);
  button2Origin: string = this.formatOrigin(null);
  subtreeOrigin: string = this.formatOrigin(null);

  ngAfterViewInit() {
    // Monitor button1 for focus origin
    this._focusMonitor.monitor(this.button1).subscribe(origin =>
      this._ngZone.run(() => {
        this.button1Origin = this.formatOrigin(origin);
        this._cdr.markForCheck();
      })
    );

    // Monitor button2 for focus origin
    this._focusMonitor.monitor(this.button2).subscribe(origin =>
      this._ngZone.run(() => {
        this.button2Origin = this.formatOrigin(origin);
        this._cdr.markForCheck();
      })
    );

    // Monitor subtree and all its elements (true for deep monitoring)
    this._focusMonitor.monitor(this.subtree, true).subscribe(origin =>
      this._ngZone.run(() => {
        this.subtreeOrigin = this.formatOrigin(origin);
        this._cdr.markForCheck();
      })
    );
  }

  ngOnDestroy() {
    // Stop monitoring when the component is destroyed
    this._focusMonitor.stopMonitoring(this.button1);
    this._focusMonitor.stopMonitoring(this.button2);
    this._focusMonitor.stopMonitoring(this.subtree);
  }

  // Formats the focus origin to be displayed
  formatOrigin(origin: FocusOrigin): string {
    return origin ? `${origin} focused` : 'blurred';
  }

  // Programmatically focus elements
  simulateFocus(button: string) {
    if (button === 'button1') {
      this.button1.nativeElement.focus();
    } else if (button === 'button2') {
      this.button2.nativeElement.focus();
    }
  }

  // Clear focus from all monitored elements
  clearFocus() {
    this.button1.nativeElement.blur();
    this.button2.nativeElement.blur();
    this.subtree.nativeElement.blur();
  }
}

i

  1. FocusMonitor Injection:
    • The FocusMonitor service is injected using the Angular inject() function, which allows us to interact with the FocusMonitor API. This service provides functionality to monitor DOM elements for focus changes.
  2. @ViewChild Decorators:
    • @ViewChild('button1'), @ViewChild('button2'), and @ViewChild('subtree') are used to reference the DOM elements we want to monitor (the buttons and the subtree). These references are passed into the component so that we can track their focus.
  3. Monitoring Focus in ngAfterViewInit:
    • In ngAfterViewInit(), which is called after the component’s view has been initialized, we set up focus monitoring using this._focusMonitor.monitor(). This method tracks the focus origin of the specified element and emits an observable each time the focus origin changes (e.g., whether the focus was triggered by the mouse, keyboard, or programmatically).
    • The focus origin for each element is updated via the this._ngZone.run() method to ensure the change detection is triggered properly when the component’s state is updated. This is crucial to update the UI in Angular’s change detection cycle.
  4. stopMonitoring in ngOnDestroy:
    • It’s important to stop monitoring elements when the component is destroyed to avoid memory leaks. The ngOnDestroy() lifecycle hook is used to call this._focusMonitor.stopMonitoring() on the monitored elements to stop the tracking when the component is no longer in use.
  5. formatOrigin() Method:
    • This method formats the focus origin into a readable string. If the origin is null, it returns “blurred”, meaning the element lost focus. Otherwise, it appends the focus origin (keyboard, mouse, etc.) to a string that is displayed in the template.
  6. Programmatic Focus Control:
    • The simulateFocus() method is responsible for focusing an element programmatically. It calls .focus() on the referenced DOM element (button1 or button2).
    • The clearFocus() method is used to remove focus from all elements using .blur(). This is useful for resetting the state.

Example CSS

.focus-monitor-container {
  padding: 20px;
}

.focus-monitor-section {
  margin-bottom: 20px;
}

button {
  margin-right: 12px;
  padding: 10px;
}

button:focus {
  border: 2px solid blue;
}

button.cdk-mouse-focused {
  background: rgba(255, 0, 0, 0.5);
}

button.cdk-keyboard-focused {
  background: rgba(0, 255, 0, 0.5);
}

button.cdk-touch-focused {
  background: rgba(0, 0, 255, 0.5);
}

button.cdk-program-focused {
  background: rgba(255, 0, 255, 0.5);
}

input,
textarea {
  margin: 10px 0;
  padding: 8px;
  width: 100%;
}

input:focus,
textarea:focus {
  border: 2px solid green;
}

 

  1. Styling for Focus Origins:
    • The .cdk-mouse-focused, .cdk-keyboard-focused, .cdk-touch-focused, and .cdk-program-focused classes are applied dynamically by FocusMonitor to the monitored elements. These CSS classes add specific background colors to visually differentiate the focus origin:
      • Red for mouse focus
      • Green for keyboard focus
      • Blue for touch focus
      • Magenta for programmatic focus
  2. General Button Styling:
    • Buttons have basic styling (margins, padding). Additionally, when they receive focus (button:focus), they get a blue border, which is common for indicating focus in UI.
  3. Input and Textarea Focus:
    • Inputs and text areas also have focus styling. When they receive focus, their border turns green, which is consistent with the keyboard-origin focus visual style.
  • Accessibility Enhancement: This approach ensures that developers can detect and respond to different focus behaviors, making it easier to build accessible forms, dialogs, or other interactive elements.

Managing Focus Traps Using cdkTrapFocus

In applications with modal dialogs or pop-ups, it’s essential to keep focus within the modal until the user explicitly closes it. The cdkTrapFocus directive helps you achieve this by trapping keyboard navigation inside a specific region.

<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
  <p>This is a modal with trapped focus. You can't tab out of here!</p>
  <button mat-button (click)="closeDialog()">Close</button>
</div>

  • Accessibility Enhancement: By trapping focus inside the modal, you prevent users from navigating out of the modal with the keyboard, ensuring they are not “lost” in the page when using a keyboard. This is particularly important for users with limited mobility who rely on keyboard navigation.

Announcing Messages for Screen Readers Using LiveAnnouncer

While we saw how this could be used in the form example, here’s a simple depiction of this feature. The LiveAnnouncer service helps announce dynamic content changes to screen readers using the aria-live region. This ensures that users are notified about form submissions, errors, or other important updates without needing to read the entire page again.

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

constructor(private liveAnnouncer: LiveAnnouncer) {}

announceMessage() {
  this.liveAnnouncer.announce('Form submitted successfully');
}

// For important alerts
this.liveAnnouncer.announce('Form error!', 'assertive');

// For less critical information
this.liveAnnouncer.announce('Data updated', 'polite', 2000);
  • Accessibility Enhancement: This ensures that screen readers pick up important messages dynamically, such as confirming form submissions, or alerting users to errors, providing a seamless user experience for visually impaired users.

List Tree Navigation with ListKeyManager and TreeKeyManager

The ListKeyManager helps manage the active option in a list, ideal for components that use the role="menu" or role="listbox" pattern. Similarly, the TreeKeyManager is used for managing navigation within tree structures (role="tree").

  • ListKeyManager: Focuses on list items and listens for keyboard events to manage the active option. It can wrap options with FocusKeyManager for direct focusable elements, or ActiveDescendentKeyManager for items that use aria-activedescendant.
  • TreeKeyManager: Useful for tree structures where focus management follows the tree’s structure and allows for easy navigation with the keyboard.
import { ListKeyManager } from '@angular/cdk/a11y';

@ViewChildren('option') options: QueryList<ElementRef>;

@HostListener('keydown', ['$event'])
onKeydown(event: KeyboardEvent) {
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
    keyManager.onKeydown(event);
    event.preventDefault();
  }
}

ngAfterViewInit() {
  const keyManager = new ListKeyManager(this.options);
  keyManager.withWrap(); // Allows wrap-around when navigating with arrow keys.
  keyManager.activeItemIndex = 0; // Set first item as active.
}
  • Accessibility Enhancement: Both of these managers allow users to easily navigate through a list or tree with keyboard commands. This is essential for accessible menus, dropdowns, or list navigation patterns.

Interactivity Checker

The InteractivityChecker is a useful utility for checking whether an element is interactable—i.e., whether it’s visible, enabled, focusable, or tabbable. This can be used to enhance accessibility by ensuring only elements that are meant to be interacted with are available for focus.

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

constructor(private checker: InteractivityChecker) {}

checkElement(element: HTMLElement) {
  const isFocusable = this.checker.isFocusable(element);
  const isTabbable = this.checker.isTabbable(element);
  
  if (isFocusable && !isTabbable) {
    console.log('Element is focusable but not in tab order');
  }
}

Styling for High Contrast Mode

High contrast mode is a feature that helps visually impaired users by applying a color scheme with high contrast. You can define specific styles to enhance the visibility of elements when the operating system’s high contrast mode is active.

@use '@angular/cdk' as cdk;

button {
  @include cdk.high-contrast(active) {
    outline: solid 2px;
    outline-offset: 3px;
  }
}
  • Accessibility Enhancement: This ensures that your elements are clearly visible to users with visual impairments, providing an optimal experience when high contrast mode is enabled.

Visually Hidden Elements with Angular CDK

<div class=”custom-checkbox”>

We’ve already seen a CSS version to hide elements from view, but we can use the cdk.a11y-visually-hidden mixin, you can hide elements without removing them from the accessibility tree.

<div class="custom-checkbox">
  <input type="checkbox" class="cdk-visually-hidden">
</div>

// Use in CSS:
@use '@angular/cdk' as cdk;

.visually-hidden {
  @include cdk.a11y-visually-hidden();
}
  • Accessibility Enhancement: This approach helps you maintain an accessible experience while visually hiding elements that are redundant or unnecessary, like form labels or controls that are part of complex interactions.

Falsifying Focus Origins

The FocusMonitor can also be used to programmatically change the focus origin. This is useful for scenarios where you want to simulate focus behavior (e.g., testing or custom behavior).

this.focusMonitor.focusVia(monitoredEl, 'keyboard');
  • Accessibility Enhancement: This allows you to simulate focus behavior for specific scenarios, which is particularly useful for accessibility testing or custom focus management.

Taking the Next Accessible Steps

In the wonderful world of technology (and the world as a whole, I suppose), the inevitable continuation of change means that there will always be next steps to take. Society will change and additional needs will arise that our code will need to address. People and users will change which will require new ideas and refined solutions. But technology will change, too. There will be new equipment, new methodologies, and new discoveries. As we walk the road ahead, our approach to designing our applications will need to be continually refined and evolving to service our ever-changing world.. 

However, the goal should remain the same. 

Improve the experience of all users by creating applications that are robust, comprehensive, inspiring, and accessible.

In this digital world we call home, as we create software and build the tools of our existence, we have the opportunity to craft how people experience their daily lives. We are creating what will bring the joy and peace, assistance and care, and what will ultimately be used as they navigate life. We have the opportunity to do so in a way where every human feels like those experiences are built for them. 

May we continue to make valuable products designed for the context of the human persons using them. 

And may that experience not only add value to our applications and companies, but to this shared human journey of the world.