Accessible form error auto-focus with Vuelidate in Vue
Tags: vue, javascript, a11y, vuelidate
Published: 2019-12-10 19:47:23

Vuelidate makes it very simple for developers to handle even the most complex cases of form validation, but what about accessibility UX? Let's take a look at some very simple practices that you can implement on your Vuelidate powered forms that will make them behave a lot more nicely for accessibility tools like screen reade

## The form

Let's first create a standard form, and apply some validation rules to our data. 

```html
    <template>
      <div>
        <form @submit.prevent="submit">
          <div>
            <label for="firstName">First Name</label>
            <input
              type="text"
    	      id="firstName"
              name="firstName"
            >
          </div>
    
          <div>
            <label for="lastName">Last Name</label>
            <input
              type="text"
    	      id="lastName"
              name="lastName"
            >
          </div>
    
          <div>
            <label for="email">Email</label>
            <input
              type="email"
              id="email"
              name="email"
            >
          </div>
    
          <button type="submit">Submit</button>
        </form>
      </div>
    </template>
```

Our form has three inputs - the first two are of type `text` and the last one of type `email`. Finally, we have a `submit` type button to trigger the `submit` event on our `form` element.

The `form` element itself has a `@submit` handler with a `prevent` modifier so that we can stop default browser behavior and process the form submit ourselves.

- To learn more about event modifiers, you can check the [official docs](https://vuejs.org/v2/guide/events.html#Event-Modifiers)

Let's now add the code that will handle the validation rules and the submit method.

```html
    <script>
    import { required, email } from "vuelidate/lib/validators";
    export default {
      name: "App",
      data() {
        return {
          firstName: "",
          lastName: "",
          email: ""
        };
      },
      validations: {
        firstName: { required },
        lastName: { required },
        email: { required, email }
      },
      methods: {
        submit() {
          // Submit the form here!
        }
      }
    };
    </script>
```

First, we import a couple of Vuelidate's [built in validators](https://vuelidate.js.org/#sub-builtin-validators): `required` and `email`.

We create a local state with `data` and set up a  property for each of our inputs, and proceed to create a `validations` object. This object in turn defines rules for each one of our inputs.

Finally, we need to head back into the `<template>` and connect our inputs to Vuelidate through `v-model`.

```html
    <div>
      <label for="firstName">First Name</label>
      <input
        type="text"
    		id="firstName"
        name="firstName"
        v-model="$v.firstName.$model"
      >
    </div>
    
    <div>
      <label for="lastName">Last Name</label>
      <input
        type="text"
        id="lastName"
        name="lastName"
        v-model="$v.lastName.$model"
      >
    </div>
    
    <div>
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        v-model="email"
        @change="$v.email.$touch"
      >
    </div>
```

Notice that for firstName and lastName we are v-modeling directly into Vuelidate's internal `$model` for each property, this allows us to not have to worry about triggering the `$dirty` state of each input on  change/input events.

For the email input, however, I have opted to v-model directly to the `data()` local state and trigger the `$touch` event manually. That way the validation won't trigger right away until after the input blur's, and the user won't be faced with an immediate error message when the `email` condition is not met because they're starting to type it out.

## Adding error messages

Let's start by adding descriptive error messages when an input's validation fails. We are going to first add a `<p>` element directly after the input and output the error for the user. 

```html
    <div>
      <label for="firstName">First Name</label>
      <input
        type="text"
        id="firstName"
        name="firstName"
        v-model="$v.firstName.$model"
      >
      <p
        v-if="$v.firstName.$error"
      >This field is required</p>
    </div>
    
    <div>
      <label for="lastName">Last Name</label>
      <input
        type="text"
        id="lastName"
        name="lastName"
        v-model="$v.lastName.$model"
      >
      <p v-if="$v.lastName.$error">This field is required</p>
    </div>
    
    <div>
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        v-model="email"
        @change="$v.email.$touch"
      >
      <p v-if="$v.email.$error">{{ email }} doesn't seem to be a valid email</p>
    </div>
```

Notice that each `p` tag is getting conditionally rendered by a `v-if` statement. This statement is checking inside the Vuelidate object `$v`, then accessing the state for each input (based on how we defined your validations and state in the previous section), and finally we access the `$error` state of this element.

Vuelidate has different states it tracks for each element, `$error` is a boolean property that will check for two conditions - it will check that the input's `$dirty` state is `true`, and that ANY of the validation rules is failing. 

The `$dirty` state is a boolean with the value of `false` by default, when an input is changed by the user and a v-model state to the `$v.element.$model` is set, it will automatically change to `true`, indicating that the contents have been modified and the validation is now ready to display errors (otherwise the form would be in default error state when loaded).

In the case of our `email` input, since we binding the `v-model` to our local state, we have to trigger the `$touch` method on the `change` event - this `$touch` will set the `$dirty` state to true.

Now that we have a clear error message for our users when the validation fails, let's go ahead and make it accessible. As it is right now, screen readers will not pick up the change and notify the user of the problem whenever the input is re-focused, which would be super confusing.

Thankfully, we have a handy tool to attach this message to our input - the  `aria-describedby` attribute. This attribute allows to attach one or more element through their `id` that describe the element. So let's modify our form to reflect this.

```html
    <form @submit.prevent="submit">
    	<div>
    	  <label for="firstName">First Name</label>
    	  <input
    	    aria-describedby="firstNameError"
    	    type="text"
    	    id="firstName"
    	    name="firstName"
    	    v-model="$v.firstName.$model"
    	  >
    	  <p
    	    v-if="$v.firstName.$error"
    	    id="firstNameError"
    	  >This field is required</p>
    	</div>
    	
    	<div>
    	  <label for="lastName">Last Name</label>
    	  <input
    	    aria-describedby="lastNameError"
    	    type="text"
    	    id="lastName"
    	    name="lastName"
    	    v-model="$v.lastName.$model"
    	  >
    	  <p v-if="$v.lastName.$error" id="lastNameError">This field is required</p>
    	</div>
    	
    	<div>
    	  <label for="email">Email</label>
    	  <input
    	    aria-describedby="emailError"
    	    type="email"
    	    id="email"
    	    name="email"
    	    v-model="email"
    	    @change="$v.email.$touch"
    	  >
    	  <p v-if="$v.email.$error" id="emailError">{{ email }} doesn't seem to be a valid email</p>
    	</div>
    	
    	<button type="submit">Submit</button>
    </form>
```

Great! If you now test out the form with a screen reader such as [ChromeVox](https://www.chromevox.com/), you can trigger a validation error and focus the element - the screen reader will now *read* the error as part of the input's information when focused, making it clearer to the user as to what is going on.

## Triggering validations on @submit

Let's take the form one step further, right now when you click the submit button nothing will happen. Let's trigger the validation check for all of the element's on our form when the user tries to submit the form.

Modify the `submit` method like this.

```js
    methods: {
      submit() {
        this.$v.$touch();
        if (this.$v.$invalid) {
          // Something went wrong 
        } else {
          // Submit the form here
        }
      }
    }
```

Two things are happening here, first we trigger the validations on every input on our form by calling `$v.$touch()`. Vuelidate will go over every input that has a validator and trigger the validation functions, so that if there are any errors the states will be updated to show it.

Vuelidate also managed a "global" state for the form which includes its own `$invalid` state, which we will use to verify if the form is in a valid state to be submitted - if it's not, we are going to help our users out by auto-focusing the first element that has an error state.

## Auto-focusing the element with an error

As it is right now, when our users click the submit button and trigger the `submit()` method, Vuelidate will verify all the inputs. If some of these inputs have errors, the `v-if` conditions for each of these inputs will be met and the error messages will be displayed.

However, screen readers will not auto-read these error messages unless we tell them to. In order to make our users' experience better, let's auto focus the input that has the problem.

First, we are going to have to go back into our form and add a `ref` attribute to each of our inputs so that we can reference and target it inside our `submit()` method.

```html
    <form @submit.prevent="submit">
      <div>
        <label for="firstName">First Name</label>
        <input
          aria-describedby="firstNameError"
          type="text"
          id="firstName"
          name="firstName"
          ref="firstName"
          v-model="$v.firstName.$model"
        >
        <p
          v-if="$v.firstName.$error"
          id="firstNameError"
        >This field is required</p>
      </div>
    
      <div>
        <label for="lastName">Last Name</label>
        <input
          aria-describedby="lastNameError"
          type="text"
          id="lastName"
          name="lastName"
          ref="lastName"
          v-model="$v.lastName.$model"
        >
        <p v-if="$v.lastName.$error" id="lastNameError">This field is required</p>
      </div>
    
      <div>
        <label for="email">Email</label>
        <input
          aria-describedby="emailError"
          type="email"
          id="email"
          name="email"
          ref="email"
          v-model="email"
          @change="$v.email.$touch"
        >
        <p v-if="$v.email.$error" id="emailError">{{ email }} doesn't seem to be a valid email</p>
      </div>
    
      <button type="submit">Submit</button>
    </form>
```

Notice that I have named all the `ref` attributes the same as their respective models. This will make the looping easier in the next step.

Now that we can target the inputs, let's modify the `submit()` method so we can loop through the different inputs and figure out which one has the error.

```js
    submit() {
      this.$v.$touch();
      if (this.$v.$invalid) {
        // 1. Loop the keys
        for (let key in Object.keys(this.$v)) {
          // 2. Extract the input
          const input = Object.keys(this.$v)[key];
          // 3. Remove special properties
          if (input.includes("$")) return false;
    
          // 4. Check for errors
          if (this.$v[input].$error) {
            // 5. Focus the input with the error
            this.$refs[input].focus();
    
            // 6. Break out of the loop
            break;
          }
        }
      } else {
        // Submit the form here
      }
    }
```

Lots of code! But fear not, we're going to break this down into easy steps.

1. First we create a `for` loop to go through each of the properties in the `$v` object. The `$v` object contains several properties, between them you will find each of the inputs that are being validated - and also some special state properties like `$error` and `$invalid` for the whole form.
2. We extract the input/property name into a variable for easy access
3. We check if the input contains the `$` character, if it does we skip this one because its a special data property and we don't care for it right now.
4. We check the `$error` state, if the `$error` state is true, it means this particular input has a problem and one of the validations is failing.
5. Finally, we use the name of the `input` as a way to access it through the instance `$refs`, and trigger the element's `focus`. This is input → ref name relationship is why earlier we went with the same naming for the ref and the v-model state.
6. We only want to focus the first element, so we call `break` to stop the loop from continuing to execute.

Try this out, now when the user triggers the form's submit and there is an error the form will automatically focus the first input with an error. 

One more slight issue, the screen reader will still not read our custom error message. We need to tell it that this `<p>` tag describing the input is going to be a "live" area that will display information and that may change.

In this case, we are going to add `aria-live="assertive"` to our error messages. This way when they appear and our focus goes to the element, the screen reader will notify the users. It will also notify them if this message changes into something else, like from a `required` validation error to a `minLength` error.

```html
    <form @submit.prevent="submit">
      <div>
        <label for="firstName">First Name</label>
        <input
          aria-describedby="firstNameError"
          type="text"
          id="firstName"
          name="firstName"
          ref="firstName"
          v-model="$v.firstName.$model"
        >
        <p
          v-if="$v.firstName.$error"
          aria-live="assertive"
          id="firstNameError"
        >This field is required</p>
      </div>
    
      <div>
        <label for="lastName">Last Name</label>
        <input
          aria-describedby="lastNameError"
          type="text"
          id="lastName"
          name="lastName"
          ref="lastName"
          v-model="$v.lastName.$model"
        >
        <p v-if="$v.lastName.$error" aria-live="assertive" id="lastNameError">This field is required</p>
      </div>
    
      <div>
        <label for="email">Email</label>
        <input
          aria-describedby="emailError"
          type="email"
          id="email"
          name="email"
          ref="email"
          v-model="email"
          @change="$v.email.$touch"
        >
        <p
          v-if="$v.email.$error"
          aria-live="assertive"
          id="emailError"
        >{{ email }} doesn't seem to be a valid email</p>
      </div>
    
      <button type="submit">Submit</button>
    </form>
```

## Wrapping up

Auto focusing elements for the user when attempting to submit an invalid form is a very nice form of accessible UX that doesn't take a lot of effort and work on our side as developers. 

With the use of attributes like `aria-describedby` and `aria-live` we have already enhanced our form into an accessible state that most form out there in the wild wild web don't implement. This can also be enhanced futher of course, but this is a great starting point!

If you want to see this example in action, I have set up a [codesandbox here](https://codesandbox.io/s/vuelidate-error-focus-accessibility-6hibn).

As always, thanks for reading and share with me your accessible form experiences on twitter at: [@marinamosti](https://twitter.com/marinamosti) 

PS. All hail the magical avocado 🥑

PPS. ❤️🔥🐶☠️