Understanding ARIA

ARIA, which stands for Accessible Rich Internet Applications, comprises a set of roles and attributes designed to enhance the accessibility of Angular applications—particularly for individuals who depend on assistive technologies such as screen readers, voice recognition software, or alternative input methods. Leveraging ARIA is fundamental to crafting inclusive, user-friendly web experiences.

WAI

The World Wide Web Consortium's Web Accessibility Initiative (WAI) developed ARIA to address accessibility gaps in dynamic web content. This same initiative is responsible for the Web Content Accessibility Guidelines (WCAG), which offer a thorough framework for A11y—you can find more details on WCAG in the series introduction. ARIA emerged in the early 2000s to compensate for limitations in native HTML, guaranteeing that contemporary, interactive single-page applications remain usable for everyone, regardless of ability.

Integrating ARIA into Angular

Within Angular, incorporating ARIA roles and attributes into your components is straightforward. You can apply them directly in your HTML view templates, treating them like any standard HTML attribute. Moreover, Angular offers built-in ARIA support through directives and bindings, which simplifies the dynamic management of ARIA properties.

For static values, just add them directly to your HTML elements or components:

<!-- Static ARIA attributes require no extra -->
<button type="button" aria-label="Close">X</button>

When you need to bind ARIA attributes dynamically, however, you should employ Angular's property binding syntax (the "[]" square brackets) with the "attr." prefix:

<!-- Dynamic ARIA attribute property binding with "attr." prefix -->
<button type="button" [attr.aria-label]="myActionLabel">…</button>

ARIA Versus Semantic HTML

Although ARIA serves as a robust mechanism for boosting accessibility, it should be treated as a fallback option. Whenever feasible, native semantic HTML elements and attributes are the preferred choice, since they come with inherent accessibility features that ARIA only replicates. Always lean toward native elements like <button>, <label>, <nav>, and <header> rather than their ARIA equivalents. For instance, opt for a <button> element instead of applying role="button" to a <div>.

ARIA Roles Explained

ARIA roles specify what an HTML element represents or how it should function when native HTML elements don't fit the bill. They instruct assistive technologies on how to understand an element and its role on the page. These roles prove especially valuable for custom components that lack inherent semantic meaning.

Frequently Used ARIA Roles

The following is a non-exhaustive selection of commonly adopted ARIA roles:

  • role="button" (prefer <button>) marks an interactive element that triggers an action.
  • role="main" (prefer <main>) designates the primary content area of your application—often the <router-outlet />.
  • role="complementary" (prefer <aside>) indicates supporting content, such as a sidebar.
  • role="navigation" (prefer <nav>) identifies a collection of navigation links, like your route navigation.
  • role="alert" reserved for critical messages that require immediate announcement.
  • role="dialog" represents a modal or popup window.
  • role="listbox" describes a widget enabling users to pick from a set of options.
  • role="tablist" organizes a tabbed interface.
  • role="tab" denotes a selectable tab within a tabbed interface.
  • role="tabpanel" serves as the container for content linked to a tab.

A complete reference of all ARIA roles is available from MDN.

Practical Examples

Consider a potential <app-dialog> component:

<app-dialog role="dialog" aria-labelledby="dialogTitle" aria-modal="true" cdkFocusTrap>
  <h2 id="dialogTitle">{{ dialogTitle() }}</h2>
  <p>{{ dialogContent() }}</p>
  <button type="button" (click)="onClose()">Close</button>
</app-dialog>

In this straightforward instance, role="dialog" signals that this is a dialog window. The aria-labelledby attribute connects the dialog to its heading, while aria-modal="true" conveys that the dialog is modal, preventing interaction with the rest of the page until dismissed. Observe that the cdkFocusTrap directive is also employed to keep keyboard navigation focus contained within the dialog while it's active.

Another illustration involves a tab interface:

<app-tablist role="tablist">
  @for (tab of tabs(); track tab.id) {
    <button role="tab" aria-controls="panel_{{ tab.id }}" id="tab_{{ tab.id }}"
            [attr.aria-selected]="activeTab() === tab.id ? 'true' : 'false'">
      {{ tab.label }}
    </button>
  } @empty {
    No tabs available.
  }
</app-tablist>
@for (tab of tabs(); track tab.id) {
  <app-tab role="tabpanel" aria-labelledby="tab_{{ tab.id }}" id="panel_{{ tab.id }}">
    {{ tab.content }}
  </app-tab>
}

Here, we have a tabbed interface. The role="tablist" designates this as a list of tabs. Each tab carries the role="tab" and references its corresponding panel via aria-controls. The aria-selected attribute shows which tab is presently active. The panels adopt role="tabpanel" and link back to their respective tabs using aria-labelledby.

ARIA Attributes Unveiled

ARIA attributes supply supplementary information about an HTML element's state, properties, or relationships. They enrich the semantic meaning of your Angular components and other HTML elements, particularly when default HTML fails to completely convey an element's behavior.

These are categorized into states and properties. States are dynamic, evolving over time, whereas properties are static, describing the element's inherent characteristics.

Widely Used ARIA Attributes

Widget attributes (states and properties)

  • aria-disabled (state/property): signals whether an element is disabled—redundant if the native disabled attribute is present.
  • aria-required (property): indicates that user input is mandatory before form submission—unnecessary with native required.
  • aria-expanded (state): conveys whether an element, like a collapsible menu, is open or closed.
  • aria-hidden (state): specifies whether an element should be hidden from assistive technologies.
  • aria-invalid (state): denotes whether an input field's value is considered valid or invalid.

Live region attributes (states)

  • aria-live: dictates how content updates are announced to the user (e.g., assertive for error notifications).
  • aria-busy: signals whether an element is undergoing updates (e.g., during a loading spinner).

Drag-and-Drop attributes (states)

  • aria-grabbed: indicates whether an element is currently being dragged (e.g., true while dragging).

Relationship attributes (properties)

  • aria-label: offers a text alternative for elements lacking visible text.
  • aria-labelledby: points to the element(s) that serve as the label for the current element.
  • aria-describedby: refers to the element(s) that provide a description of the current element.
  • aria-controls (state): lists the element(s) whose content is managed by the current element.

Once more, MDN provides the complete list of ARIA attributes.

Examples in Action

Take a toggle button that utilizes aria-label and aria-expanded:

<button
  type="button"
  aria-label="Toggle navigation menu"
  [attr.aria-expanded]="isMenuOpen() ? 'true' : 'false'"
  (click)="onToggleNav()">
  <i class="icon-menu"></i>
</button>

In this example, the aria-label attribute supplies a text alternative for the button, clarifying its function. The aria-expanded attribute reveals whether the navigation menu is currently open or closed. This is crucial for screen reader users, as it aids their comprehension of the button's current state.

Now consider aria-live paired with role="alert" for an error message:

@let showFromErrors =
  flightSearchForm.controls.from.errors &&
  flightSearchForm.controls.from.touched;

<input
  [...]
  id="fromAirport"
  [attr.aria-invalid]="!!showFromErrors"
  [attr.aria-describedby]="showFromErrors ? 'fromAirportErrors' : null"
/>
@if (showFromErrors) {
  <app-flight-validation-errors
    aria-live="assertive"
    role="alert"
    id="fromAirportErrors"
    fieldLabel="From"
    [errors]="flightSearchForm.controls.from.errors"
  />
}

In this scenario, the aria-live attribute is configured to assertive, prompting the screen reader to announce the error message promptly upon its appearance. The role="alert" marks this as a critical message. Furthermore, the aria-invalid attribute signals that the input field contains validation errors, and the aria-describedby attribute links the error message to the input field. It's worth noting that aria-describedby is set to null when no errors exist, ensuring it's omitted from the rendered DOM when unnecessary.

Workshop Opportunities

For those aiming to expand their Angular knowledge, we provide a variety of workshops available in both English and German:

Final Thoughts

Adopting ARIA within Angular is vital for constructing web applications that are both accessible and welcoming. By skillfully applying ARIA roles and attributes, developers can fill gaps where native HTML falls short, guaranteeing a positive experience for all users. This strategy not only satisfies accessibility standards but also establishes a foundation for user-focused, future-ready design.

The next installment of our A11y blog series will delve into Accessible Angular Forms.

This article was authored by Alexander Thalhammer. Connect with me on Linkedin, X, or GitHub.

Further Reading