ARIA (short for Accessible Rich Internet Applications) roles and attributes are used to improve Accessibility of our Angular apps – especially for users who rely on assistive technologies like screen readers, voice control, or alternative input devices. Using ARIA is essential for building inclusive, user-friendly web apps.
About ARIA
ARIA was developed by the World Wide Web Consortium’s Web Accessibility Initiative (WAI) to enhance the accessibility of dynamic web content. The WAI also is the organization behind the Web Content Accessibility Guidelines (WCAG), which provide a comprehensive framework for A11y – more about WCAG in the intro of this A11y blog series.
Image may be NSFW.
Clik here to view.
Originating in the early 2000s, ARIA was created to bridge the gaps in native HTML, ensuring that modern, interactive and single-page applications are usable by people with disabilities Image may be NSFW.
Clik here to view.
ARIA in Angular
In Angular, ARIA roles and attributes can be easily integrated into your components. You can use them directly in your HTML view templates, just like any other HTML attribute. Angular also provides built-in support for ARIA through directives and bindings, making it easier to manage ARIA properties dynamically.
When using static values, you can simply add them to your HTML elements or components:
<!-- Static ARIA attributes require no extra -->
<button type="button" aria-label="Close">X</button>
However, when you want to bind ARIA attributes dynamically, you should use Angular's property binding syntax ("[]" square brackets) including the "attr." prefix:
<!-- Dynamic ARIA attribute property binding with "attr." prefix -->
<button type="button" [attr.aria-label]="myActionLabel">…</button>
ARIA vs. Semantic HTML
While ARIA is a powerful tool for enhancing accessibility, it should be used as a last resort. Native semantic HTML elements and attributes are preferred whenever possible, as they provide built-in accessibility features that ARIA does not have to replicate. Always prefer native elements like <button>
, <label>
, <nav>
and <header>
over their ARIA counterparts. For example, instead of using role="button"
on a <div>
, use a <button>
element.
ARIA Roles
Speaking about ARIA roles, they define what an HTML element is or how it should behave – when native HTML elements are not applicable. They tell assistive technologies how to interpret an element and its purpose within the page. Roles are particularly useful for custom components that don’t have native semantic meaning.
Most commonly used ARIA roles
Here is a not exhaustive list of some ot the most commonly used ARIA roles:
role="button"
(prefer<button>
) indicates an interactive element that triggers an action.role="main"
(prefer<main>
) the main content of your app – typically<router-outlet />
.role="complementary"
(prefer<aside>
) denotes content that complements – like a sidebar.role="navigation"
(prefer<nav>
) denotes a section of navigation links – like your route nav.role="alert"
used for important messages that should be immediately announced.role="dialog"
signifies a modal or popup window.role="listbox"
used for a widget that allows the user to select from a list of options.role="tablist"
composes a tabbed interface.role="tab"
represents a selectable tab in a tabbed interface.role="tabpanel"
the container for the content associated with a tab.
Find a comprehensive list of all ARIA roles by MDN.
Examples
This could be a <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 simple example, the role="dialog"
indicates that this is a dialog window. The aria-labelledby
attribute associates the dialog with its title, and aria-modal="true"
indicates that the dialog is modal, meaning it prevents interaction with the rest of the page until closed. Note that we also use the cdkFocusTrap
directive to ensure that (keyboard navigation) focus is trapped within the dialog while it is open.
Another example could be 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>
}
In this example, we have a tabbed interface. The role="tablist"
indicates that this is a list of tabs. Each tab has the role="tab"
and is associated with its corresponding panel using aria-controls
. The aria-selected
attribute indicates which tab is currently selected. The panels have the role="tabpanel"
and are associated with their respective tabs using aria-labelledby
.
ARIA Attributes
ARIA attributes provide additional details about an HTML element’s state, properties or relationships. They enhance the semantic meaning of your Angular components and other HTML elements, especially when default HTML does not fully describe an element’s behavior.
We distinguish between states and properties. States are dynamic and can change over time, while properties are static and describe the element's characteristics.
Most commonly used ARIA attributes
Widget attributes (states and properties)
aria-disabled
(state/property): indicates whether an element is disabled or not – not necessary if nativedisabled
is used.aria-required
(property): indicates that user input is required before submitting a form – not necessary if nativerequired
is used.aria-expanded
(state): communicates whether an element, such as a collapsible menu, is expanded or collapsed.aria-hidden
(state): indicates whether an element should be exposed to assistive technologies.aria-invalid
(state): indicates whether the value of an input field is valid or invalid.
Live region attributes (states)
aria-live
: specifies how updates to content should be announced to the user (e.g.,assertive
for error messages).aria-busy
: indicates whether an element is currently being updated (e.g., loading spinner).
Drag-and-Drop attributes (states)
aria-grabbed
: indicates whether an element is currently being dragged (e.g.,true
when dragging).
Relationship attributes (properties)
aria-label
: provides a text alternative for elements that may not have visible text.aria-labelledby
: identifies an element (or elements) that labels the current element.aria-describedby
: identifies an element (or elements) that describes the current element.aria-controls
(state): references the element(s) whose content is controlled by the current element.
And here is again the full list of all ARIA attributes by MDN.
Examples
A toggle button using 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 simple example, the aria-label
attribute provides a text alternative for the button, indicating its purpose. The aria-expanded
attribute indicates whether the navigation menu is currently open or closed. This is important for screen reader users, as it helps them understand the state of the button.
Using aria-live
combined 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 example, the aria-live
attribute is set to assertive
, which means that the screen reader will announce the error message immediately when it appears. The role="alert"
indicates that this is an important message. Additionally, the aria-invalid
attribute is used to indicate that the input field has validation errors and the aria-describedby
attribute is used to associate the error message with the input field. Note that the aria-describedby
attribute is set to null
when there are no errors. This ensures that it's not present in the rendered DOM when not needed.
Accessibility Workshop
For those looking to deepen their Angular expertise, we offer a range of workshops – both in English and German:
- Image may be NSFW.
Clik here to view.Accessibility Workshop
- Image may be NSFW.
Clik here to view.Best Practices Workshop (including accessibility related topics)
- Image may be NSFW.
Clik here to view.Performance Workshop
Conclusion
Implementing ARIA in Angular is essential for building web apps that are both accessible and inclusive. By leveraging ARIA roles and attributes appropriately, developers can bridge gaps where native HTML falls short, ensuring that all users have a positive experience. This approach not only meets accessibility standards but also lays the foundation for user-centric and future-proof design. For more information on Angular and accessibility, check out my Ally blog series.
This blog post was written by Alexander Thalhammer. Follow me on Linkedin, X or giThub.
References
- Accessibility in Angular Applications by Zama Khan Mohammed
- Accessibility in Angular 17 by Amir Saeed
- ariaCurrentWhenActive – official docs
- ARIA roles
- ARIA attributes
The post ARIA roles and attributes in Angular appeared first on ANGULARarchitects.