Creating Accessible Forms in React

[object Object]
Sara MitevskaJune 7th, 2024
June 7th, 2024 18 minutes read
https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCSrIHSGBNhlv905RIyv7zDFlDT2eKzhvvtBh57wyhWPAThRbJ8dWwJFYfrqj0RA_7lKmuJUFHekRdeM8FBkLn5G1a2ywX3IGc1RkGilW-PaIJWbAp7THvAuGrSj4I9J3pVwNcvhOuP04BdUxocjGxZ-uM6mZeF8s6zkI61MiBjXDqCUBXgjyBUAj-gT_r/w640-h640/9b362fb5-6543-4d54-9d6e-e6a9453e4938.webp
accessible forms in react

Why is accessibility in web forms important

Accessibility in web forms ensures that all users, including those using assistive technologies such as screen readers and keyboard-only navigation, can interact with and complete web forms easily. There are already established accessibility standards and best practices that developers can follow to create inclusive online web forms that improve the overall user experience by making it usable for everyone. (Some resources are included at the end of this post).

Semantic HTML form elements

One of the most important step to creating accessible forms is using the proper semantic HTML form elements such as: <form>, <input>, <label>, <select>, <textarea>, <button>, <fieldset>, <legend>, <datalist>, <output>, <option>, <optgroup>. All of these elements clearly describe their meaning in a human and machine-readable way and provide context to web content, enabling assistive technologies to interpret and interact with the content accurately.

Properly structured semantic HTML elements also enable better keyboard navigation essential for users who rely on keyboards or other input devices other than mouse. Using the correct element such as <form> will ensure that pressing the Tab key through a form follows a logical order, that will make form completion easier, or using a <button> element will trigger form submission by default when the Enter key is pressed. In the same way, associating a <label> with an <input> helps screen readers announce the label when the input field is focused, which helps users understand the purpose of the field.

In some situations, custom form elements might be needed if the provided HTML elements are not enough for the purpose of if a more customised look is required. In which case it is very important to make the custom elements accessible using the proper ARIA (Accessible Rich Internet Applications) roles and attributes. However, relying on semantic HTML elements whenever possible will certainly reduce the need for additional ARIA roles and properties which will minimise complexity and potential errors.

How to create accessible forms

Placeholders

Most often, placeholder text is used to provide instructions or an example of what kind of data is required for a certain form field. It is usually displayed with lower color contrast and it disappears when the user starts typing. Placeholders can provide valuable guidance for many users, however it is important to always use it alongside a label as assistive technologies do not treat placeholders as labels.

Labels

Label elements provide clear, descriptive text associated with form fields that allow all users to better understand the purpose of each field. Here are a few approaches of using label elements when creating accessible forms.

Explicitly associating labels with form fields:

The most common and recommended approach is using the for attribute on a <label> element to associate it with an <input> element by matching the for attribute with the id of the input element.

  <label for="username">Username</label>
  <input type="text" id="username" name="username"/>

Wrapping the form field in label element:

Sometimes the id of a form field might not be known or even present. In cases like this, the <label> is used as a container for both the label text and the form field so that the two are associated implicitly.

  <label>
    Username
    <input type="text" name="username"/>
  </label>

However, explicitly associating labels is generally better supported by assistive technology.

Using additional instructions aside from labels

In some cases, additional instructions aside from the label might be required. Such as: a helper text below the input field or an additional description bellow a label element. To make the form accessible in scenarios like this, you can use the aria-labelledby and aria-describedby attributes. Here is an example:

  <label id="dateLabel" for="dateOfBirth"> Date of birth:</label>
  <input type="text" name="dateOfBirth" id="dateOfBirth" aria-labelledby="dateLabel dateHelperText">
  <span id="dateHelperText">MM/YYYY</span>                                                                    
  <label id="dateLabel" for="dateOfBirth">Date of birth:</label>
  <input type="text" name="dateOfBirth" id="dateOfBirth" aria-labelledby="dateLabel" aria-describedby="dateHelperText">
  <span id="dateHelperText">MM/YYYY</span>                                                                    

Hiding the label

Sometimes, the design requires omitting a label from the form field, but it is still necessary for a fully accessible form. Fortunately, there is a solution you can apply in situations like this.

Hiding the label visually

The most common approach used is to hide the label visually but keep it available to the assistive technology devices. It can be achieved by using css to hide the element.

  <label for="example" class="visually-hidden">Example Label</label>
  <input type="text" id="example" name="example"/>   
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
  }

Please note that using visibility: hidden will not work properly in this case. The label must be hidden by displaying it in a 1 pixel area like in the example in order for the screen readers to interpret it.

Labelling buttons

When it comes to labeling buttons, there are also a few possible solutions to make it accessible. Let's take a look at some.

Adding the label inside the element

The standard and most common approach is adding a visible text inside the button element.

  <button>Submit</button>
Using aria-label Attribute

This attribute provides an accessible name to button elements that don't have visible text, usually buttons that contain an icon.

  <button aria-label="Submit">✔️</button>
Using title Attribute

Alternatively, the text can be placed in the title attribute. This attribute can provide additional information, although it is less preferred than aria-label.

Visually hidden label

A more accessible alternative for buttons that don't have visible text is an approach similar to the visually hidden label: text that is visually hidden using CSS next to the icon.

  <button>
    <span class="visually-hidden">Submit</span> ✔️
  </button>
Using the value attribute

When using an <input type="button"> for a button element, the label can be placed in the value attribute.

  <input type="button" value="Submit" />
Using image as button

If the image button <input type="image"> is used, the label is set in the alt attribute.

  <input type="image" src="button.png" alt="Submit">

Grouping elements

Grouping form elements using the appropriate HTML elements such as <fieldset> and <legend> provides semantic meaning to assistive technologies. Screen readers can understand the relationship between form elements within the group, making it easier for the users to understand the form as well.

Grouping form elements also allows users to navigate between related fields more easily using the keyboard. Users can typically jump between form fields within the group using the Tab key, improving the overall usability and accessibility of the form.

When a group of form elements is focused, assistive technologies can announce the group's label or legend, providing users with context about the purpose of the group. This helps users better understand the structure of the form.

Grouping related fields using Fieldset and Legend

The <fieldset> element semantically groups related form fields which allows assistive technologies to interpret and understand their relationship. Visually grouped elements also make the form easier to understand and use by any user.

The <fieldset> element is used in combination with the <legend> element, which provides a caption for the group. Here is an example:

  <form>
    <fieldset>
      <legend>Personal Information</legend>
      <label for="firstName">First Name:</label>
      <input type="text" id="firstName" name="firstName">
      <label for="lastName">Last Name:</label>
      <input type="text" id="lastName" name="lastName">
    </fieldset>
    <fieldset>
      <legend>Address</legend>
      <label for="street">Street:</label>
      <input type="text" id="street" name="street">
      <label for="city">City:</label>
      <input type="text" id="city" name="city">
    </fieldset>
    <button type="submit">Submit</button>
  </form>

Radio Buttons, Checkboxes or related fields must also be grouped using <fieldset> with corresponding <legend>.

For select elements with groups of options, the <optgroup> element can be used to indicate such groups. The label attribute of the <optgroup> element is used to provide a label for the group.

Grouping related fields with WAI-ARIA

When using <fieldset> and <legend> is not an option (perhaps the design requires more custom element), the same grouping of elements can be achieved by associating the related fields using WAI-ARIA attributes. Such attributes are: aria-labelledby and aria-describedby. For example, aria-labelledby can link a group of related fields to a heading that describes their collective purpose ensuring that screen readers correctly interpret this relationship to the users.

Additionally, applying the attribute role="group" on an element such as <div> can be used to define a logical grouping of related fields, providing a semantic structure that assistive technologies can read.

Let's modify the code of the previous example by using WAI-ARIA attributes to associate related fields without using <fieldset> and <legend>:

  <form>
    <div role="group" aria-labelledby="personalInfoHeading">
      <h2 id="personalInfoHeading">Personal Information</h2>
      <label for="firstName">First Name:</label>
      <input type="text" id="firstName" name="firstName">
      <label for="lastName">Last Name:</label>
      <input type="text" id="lastName" name="lastName">
    </div>
    <div role="group" aria-labelledby="addressHeading">
      <h2 id="addressHeading">Address</h2>
      <label for="street">Street:</label>
      <input type="text" id="street" name="street">
      <label for="city">City:</label>
      <input type="text" id="city" name="city">
    </div>
    <button type="submit">Submit</button>
  </form>

Keyboard Navigation

As mentioned in this guide, enabling proper keyboard navigation is a very important step when creating accessible forms to provide a good user experience to users that use keyboard for navigating though the web. If you rely mostly on using semantic HTML elements, this is not something you need to worry about. However, when a certain custom element needs to be incorporated in a web form, you might need to follow some of the establish standards on how it should function so that you can provide proper keyboard navigation and full accessibility in your form.

Depending on the type and purpose of element, the standards can slightly differ. For example, creating a custom <select> (comobox) element might require different keyboard navigation than a <button> element would. Since this is a bit larger topic, in this guide we'll only mention a few of the most common requirements for a custom form field.

For example, each element contained in a form should be focused and interacted with using the Tab key. This is usually achieved by using the tabindex attribute. Some elements such as <select>, <input> or <button> should have a keyDown event listener on Enter key that will allow the specific option to be selected, the specific button clicked or the form to be submitted. In the same way, pressing the Escape key should dismiss open popups/menus and similar elements.

All standards, rules and patterns for specific elements can be found on the WAI-ARIA documentation.

Form validation and Errors

Effective form validation includes providing clear information about required and optional fields along with concise error messages that are accessible to screen readers and other assistive technologies.

Required fields

Required fields in forms are usually marked by using either the required attribute, the * symbol next to the label or both. While this may provide a good visual experience for some users, to make sure that all users can easily interact with the form additional attributes such as aria-required should be used, especially when using custom elements other than the semantic HTML form elements.

Most current web browsers automatically set the value of aria-required to true when the HTML5 required attribute is present.

Please note that the aria-required attribute, like all ARIA states and properties, only helps the screen readers and such devices to interpret an element as required but they have no impact on element functionality. Functionality and behaviour must be added in with JavaScript.

Displaying errors

Displaying errors in forms can be as simple as showing a certain text containing a message explaining the error for incorrect input. When using the semantic HTML elements like input with the specific type attribute such as: 'date', 'number', 'tel', 'email' etc. such message is shown by default that is already adapted for accessibility. But when a more customised error message is needed, specific attributes must also be applied to ensure accessibility.

The attributes aria-invalid and aria-errormessage should be used together to indicate an error in a form field. Both aria-invalid and aria-errormessage are applied to the input field while the aria-invalid is a boolean value indicating if there is an error or not, and aria-errormessage contains the id of the element where the error message is shown. The aria-errormessage attribute should only be used when the value of a field is not valid that is when aria-invalid is set to 'true'. If the field is valid and you include the aria-errormessage attribute, make sure the element referenced is hidden, as the message it contains is not relevant. Here is an example:

  <label for="email">*Email address:</label>
  <input
    id="email"
    type="email"
    name="email"
    aria-invalid="true"
    aria-errormessage="emailError"
  />
  <span id="emailError">Incorrect email</span>

However, the screen reader won't automatically read the error message when it appears solely based on the presence of aria-errormessage attribute. To allow announcing the error message when there is an error on change on the input or on form submit, you should apply another attribute on the element that contains the error message: aria-live. This attribute can have one of the three values:

  • assertive - Indicates that updates to the region have the highest priority and should be presented to the user immediately.
  • off (default) - Indicates that updates to the region should not be presented to the user unless the user is currently focused on that region.
  • polite - Indicates that updates to the region should be presented at the next graceful opportunity, such as at the end of speaking the current sentence or when the user pauses typing.

FormFusion and accessibility

If you are like me and you are not a fan of repeting code or you simply don't want to worry too much about accessibility you can always use libraries such as FormFusion that make creating forms easy and worry-free. The library offers fully accessible form elements easy to customise along with built-in validation.

Here is an example of the form mentioned previously, built using FormFusion:

  import { Form, Input } from 'formfusion';
  import './App.css';

  function App() {
    return (
      <Form>
        <fieldset>
          <legend>Personal Information</legend>
          <Input
            type="alphabetic"
            id="firstName"
            name="firstName"
            label="First name"
            classes={{ root: 'formControl', error: 'formControl__error' }}
          />
          <Input
            type="alphabetic"
            id="lastName"
            name="lastName"
            label="Last name"
            classes={{ root: 'formControl', error: 'formControl__error' }}
          />
          <Input
            type="email"
            id="email"
            name="email"
            label="Email"
            required
            classes={{ root: 'formControl', error: 'formControl__error' }}
          />
        </fieldset>
        <fieldset>
          <legend>Address</legend>
          <Input
            type="text"
            id="street"
            name="street"
            label="Street"
            classes={{ root: 'formControl', error: 'formControl__error' }}
          />
          <Input
            type="alphabetic"
            id="city"
            name="city"
            label="city"
            classes={{ root: 'formControl', error: 'formControl__error' }}
          />
        </fieldset>
        <button type="submit">Save</button>
      </Form>
    );
  }

  export default App;

FormFusion will automatically add all of the necessary attributes to make the form fully accessible. It will also handle the validation and how the errors are displayed depending on the selected validation method (validateOnChange or validateOnBlur). The final HTML structure of this code in the browser, assuming that the email field was already interacted with, will look like this:

  <form class="FormFusion">
    <fieldset>
      <legend>Personal Information</legend>
      <div class="FormFusion-Input__root formControl">
        <label for="firstName" class="FormFusion-Input__root__label">First name</label>
        <input 
          id="firstName" 
          name="firstName" 
          class="FormFusion-Input__root__field" 
          type="text" 
          pattern="^[a-zA-Z\s]+" 
          data-type="alphabetic" 
          value="" 
          aria-invalid="false" 
        />
        <span
          class="FormFusion-Input__root__error formControl__error"
          id="FormFusion-firstName-error"
          aria-live="polite">
        </span>
      </div>
      <div class="FormFusion-Input__root formControl">
        <label for="lastName" class="FormFusion-Input__root__label">Last name</label>
        <input 
          id="lastName" 
          name="lastName" 
          class="FormFusion-Input__root__field" 
          type="text" 
          pattern="^[a-zA-Z\s]+" 
          data-type="alphabetic" 
          value="" 
          aria-invalid="false" 
        />
        <span
          class="FormFusion-Input__root__error formControl__error"
          id="FormFusion-lastName-error"
          aria-live="polite">
        </span>
      </div>
      <div class="FormFusion-Input__root formControl">
        <label for="email" class="FormFusion-Input__root__label">Email</label>
        <input 
          id="email" 
          name="email" 
          required="" 
          class="FormFusion-Input__root__field" 
          type="email" 
          data-type="email" 
          value="" 
          aria-invalid="true" 
          aria-errormessage="FormFusion-email-error" 
        />
        <span
          class="FormFusion-Input__root__error formControl__error"
          id="FormFusion-email-error"
          aria-live="polite">
          Please include an '@' in the email address. 'sa' is missing an '@'.
        </span>
      </div>
    </fieldset>
    <fieldset>
      <legend>Address</legend>
      <div class="FormFusion-Input__root formControl">
        <label for="street" class="FormFusion-Input__root__label">Street</label>
        <input 
          id="street" 
          name="street" 
          class="FormFusion-Input__root__field" 
          type="text" 
          value="" 
          aria-invalid="false" 
        />
        <span
         class="FormFusion-Input__root__error formControl__error"
         id="FormFusion-street-error"
         aria-live="polite">
       </span>
      </div>
      <div class="FormFusion-Input__root formControl">
        <label for="city" class="FormFusion-Input__root__label">City</label>
        <input 
          id="city" 
          name="city" 
          class="FormFusion-Input__root__field" 
          type="text" 
          pattern="^[a-zA-Z\s]+" 
          data-type="alphabetic" 
          value="" 
        />
        <span
          class="FormFusion-Input__root__error formControl__error"
          id="FormFusion-city-error"
          aria-live="polite">
        </span>
      </div>
    </fieldset>
    <button type="submit">Save</button>
  </form>

The full code for this example can be found on Github and Stackblitz.

Conclusion

Accessibility in web forms is crucial for creating inclusive web applications. By using semantic HTML elements, following best practices, and applying ARIA roles and attributes, developers can ensure all users, including those that use assistive technologies, can easily complete forms. Well-structured forms with clear labels, logical tab order, and accessible error messages improve usability for everyone. Tools like FormFusion simplify this process by providing accessible form components. Prioritising accessibility from the start ensures a better user experience for all and contributes to a more inclusive digital world.

Resources

https://developer.mozilla.org/en-US/docs/Web/Accessibility
https://www.w3.org/WAI/ARIA/apg/patterns/
https://www.w3.org/WAI/standards-guidelines/aria/