Creating Accessible Forms with Angular

Creating Accessible Forms with Angular

When searching for examples of Angular forms and how they're set up, we noticed a trend that may have been ported over from AngularJS examples: a suggested approach when designing and developing forms to include inline validation and reserving the submit control in a disabled state until valid data has been entered. While this paradigm may seem more modern and may help the user complete the form successfully, there are a few usability and accessibility issues that appear when this workflow is implemented.

We'll explore these concepts in-depth, discuss why they might not be the best solution for your users, and offer practical, more inclusive solutions which can be implemented within your Angular forms.

When reviewing the example approaches provided, we see two trends in particular: encouraging inline-validation of each form control after it's been "touched" (essentially when the user moves away from the input ) and keeping the submit button in a disabled state until each validation test has been satisfied.

Inline validation

We typically recommend not using inline validation, as this method has the potential to cause the user a bit of confusion or frustration at times. Some examples illustrating this include:

  • When someone using a screen reader is navigating through a form to get a lay-of-the-land, trying to read what each field is and the expected data
  • Someone who might want to fill in fields in a different order
  • When someone leaves a field to go look something up to be certain of what data to place within the form control

These simple actions trigger the error message and the result can be quite irritating.

In our own usability studies, people often grumble or curse out loud when error messages appear before they've even started filling out forms!

Additional accessibility issues appear when the user navigates away from a field; the error message is displayed visually but not to assistive technology. In order to hear the error message, the user needs to navigate backward through the form. This would be an unexpected required action to hear error messages.

Disabled state

With the submit button set as disabled by default, only after all of the form validation rules have been satisfied does the submit button become available for use. Having this in place might lead to a confusing or frustrating user experience; there's no indication as to why the submit button would be disabled after filling in the form.

Consequently, disabled buttons can present two additional challenges for individuals:

  1. People who have low vision might not be able to perceive the button text in the dimmed state.
  2. Some assistive technology will skip disabled elements; screen reader users may not get a full picture of what is available in the interface, leading to feelings of uncertainty.

Example form

In the example linked below, we have a common "Contact Us" style form. This form features the workflow described above, including the trends of testing the input validity after the control has been interacted with and the disabled submit button by default:

Behind the scenes

Here's what's happening in the code to make this current workflow take place:

For each form input control requiring validation, there exists an *ngIf directive. This directive tests two conditions:

  1. Test the validation condition using the contactForm.controls[name].hasError() method
  2. Test the state of the control using the contactForm.controls[name].touched flag
  *ngIf="contactForm.controls['firstName'].hasError('required') && contactForm.controls['firstName'].touched"
  You must include a First Name.

Since each condition statement uses the "and" clause, both conditions need to be met in order to show the input error message. Basically, these conditions state, "When the user moves away from the input control, check the validity of the data. If the data is not valid, show the error message."

The submit button features the [disabled]="!contactForm.valid" property binding. This binding holds the button in a disabled state until the form data is completely valid. Only then will the user be able to find and activate the button.

<button type="submit" [disabled]="!contactForm.valid">Submit</button>

Accounting for accessibility with this workflow

To alleviate some of these potential pain points for your users, we recommend taking a different approach altogether. Consider making the following changes to your form workflow:

1) Allow validation to happen on initial form submission by keeping the submit button enabled. This will allow the user to easily explore the form and become comfortable with the form structure on the first run through. Even if the form fields have errors, allow users to submit the form on their terms, when they feel comfortable to do so. To do this, we simply remove the [disabled] property binding from the submit button.

<button type="submit">Submit</button>

2) Add a new class property, submitted , which will serve as a boolean flag. By default, its value will be false . When the form submit button is clicked, update its value to true . This property will be used within the template as part of the *ngIf directive when checking our validation rules and whether to show an error message.

export class ContactFormComponent {
 submitted: boolean;
 // … submitForm(value: any) {
 this.submitted = true;
 // …

3) When an input is in an error state, output the error message text within its corresponding label . This will serve as a reminder of the error and the expected value when the control is navigated to, as well as any other controls in an error state when the user moves forward through the form content.

<label for="firstName">
    *ngIf="contactForm.controls['firstName'].hasError('required') && submitted"
    You must include a First Name.

Bonus: This also has the benefit of creating a much larger click/tap area for people who are using a mouse or mobile device, since the labels themselves are targets!

We can check if the input is in an error state by adjusting the *ngIf directive. Continue using the controls[name].hasError() method to test the validity of the input value, but remove the controls[name].touched flag as we no longer need this property. Here's where we can use the new submitted property to see if the form has been submitted. If the input value is invalid and the form has been submitted , display the error message.

Turbocharging your forms with next-level accessibility

So far we've addressed the flow issues with this approach by removing the disabled state of the submit button by default and by validating each input after initial submission. There are a few extra steps we can take to greatly increase the overall accessibility of any large/complex form. To avoid over-engineering, short forms such as a login form, would be exempt from the following:

1) If there are errors to display, output each error message in a ul list at the top of the form, above the form fields. When this list is visually presented, programmatically shift keyboard focus to the top of the list, preferably a heading element. This will provide a notification that something happened after clicking the submit button , that there are errors to be fixed and the total number of errors (via ul list item count). Implementing each error message in a list helps users to understand and then navigate to correct the errors. In the submitForm() method, select the heading and apply focus() when the form returns invalid.

<ul id="error-list">
  <li *ngIf="contactForm.controls['firstName'].hasError('required')"></li></ul>

2) For each error message in the ul list, implement the text as an link which points to the corresponding input control, via matching its id value with the href attribute of the link. With this in place, users will be able to hear the error message text and have a shortcut directly to the input in question, which is helpful for users with dexterity issues who rely on the keyboard.

<a href="#firstName">You must include a First Name</a>
<!-- … -->

3) Ensure that any required fields are conveyed to all users by having a visual "*" (required) indicator with a text alternative for assistive technology users. This will help all users understand which parts of the form need to be completed and which can be skipped. This could even be an SVG image with alternative text !

<span aria-hidden="true">*</span>
<span class="visuallyhidden">Required</span>

Putting it all together

Here's our example form again, but with the recommendations as described above:

With these changes in place, anyone relying on assistive technology or those who need a little more guidance in completing a form will have a clearer understanding of how the form is structured and the current state of the form, if there are any errors and how to address them. If there are changes to be made on submit, focus will be brought up to the error listing. From there, error links will move focus directly to the form input in question. From this point, error messages will be announced when traversing the form, and any changes to the data can easily be made with greater confidence on the part of the user.

Keep in mind, this recommended workflow is not specific to Angular forms. The techniques and concepts explored in this post can be applied to any form, using any JavaScript framework or library.

Angular is able to support accessibility features, but developers still need to make sure the implementation meets the needs of all users.

The key takeaway is to make sure your users are able to discover the layout and inputs of the form, and to complete their task of filling out the form you've designed. They'll have confidence and assurance that they've done the right thing, and both of your goals are met!

Consider enhancing the flow of your Angular forms with these suggestions in order to help guide your users in successfully completing forms with ease.

Back to blog