Image

vue-3-forms

16 min read
Last update: December 19, 2021

Basic a11y for our components

Welcome back!

At the end of the last lesson we discussed the importance of incorporating accessibility into your first round of development. I can not stress this enough: Accessibility is not a secondary task that you come back to after your app is working. It is a primary concern that needs to be addressed as part of your development process.

In this course I decided to keep it separate for educational reasons, introducing one concept at a time and building upon those concepts incrementally. We now have the conceptual groundwork laid out to add in our accessibility features.

We will go over what I consider some of the very basic accessibility concepts that you need to keep fresh in mind when developing forms. These concepts are not technically Vue-specific, but we will learn how to apply them in the context of our Vue form components.

Let’s dive right in.


Appropriate types

In HTML we have a wide variety of input elements to craft our forms, but one element in particular rules them all. The catch-all input allows us the flexibility of creating text inputs, but we can also transform it into checkboxes and radio buttons with the type property.

A common mistake is to ignore this type property when creating text inputs. Most of us know and commonly use two regularly: type email and password.

When using a specific type in an input element, not only do we get better autocompletion for our form, but it also allows screen readers to better understand what type of data we want to retrieve from the user. A type of tel for example, will provide the user on a mobile phone with a handy numeric keyboard with phone symbols like + * #.

Your users with mobility problems will definitely be grateful for this one!

Bottom line: Don’t forget to set your type, even when the input is not of type password or email.

Here is a list of the available types for an input element:

  • button
  • checkbox
  • color
  • date
  • datetime-local
  • email
  • file
  • hidden
  • image
  • month
  • number
  • password
  • radio
  • range
  • reset
  • search
  • submit
  • tel
  • text
  • time
  • url
  • week

Use Fieldset and Legend

Two often overlooked or under-taught elements in HTML are fieldset and legend.

In forms, usually we group our inputs logically. For example, you would usually code your form to first ask the user for their personal data like Name, Last Name, and Phone. Later on, another section may ask them for a shipping address.

For accessible users, this information may not be as immediately available without having to tab through the whole form, this is where <fieldset> and <legend> come to play.

    You should always try to wrap up sections of your form inside a `fieldset` element. This will logically group the inputs inside of it. Then, the first element of the `fieldset` will be a `legend` element which will provide a _Title_ for that particular fieldset.

    If for some reason you don’t want the `legend` to show on your form (usually because of design reasons), you can always position it _absolutely_, outside of the visible screen.

    For our current form in `SimpleForm.vue`, we can wrap up our logical sections inside `fieldset` like in the following example:

    **📃 SimpleForm.vue**

    ```html
    <template>
        <div>
            <h1>Create an event</h1>
            <form @submit.prevent="sendForm">
                <BaseSelect :options="categories" v-model="event.category" label="Select a category" />

                <fieldset>
                    <legend>Name & describe your event</legend>

                    <BaseInput v-model="event.title" label="Title" type="text" />

                    <BaseInput v-model="event.description" label="Description" type="text" />
                </fieldset>

                <fieldset>
                    <legend>Where is your event?</legend>

                    <BaseInput v-model="event.location" label="Location" type="text" />
                </fieldset>

                <fieldset>
                    <legend>Pets</legend>

                    <p>Are pets allowed?</p>
                    <div>
                        <BaseRadioGroup v-model="event.pets" name="pets" :options="petOptions" />
                    </div>
                </fieldset>

                <fieldset>
                    <legend>Extras</legend>
                    <div>
                        <BaseCheckbox v-model="event.extras.catering" label="Catering" />
                    </div>

                    <div>
                        <BaseCheckbox v-model="event.extras.music" label="Live music" />
                    </div>
                </fieldset>

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

            <pre>{{ event }}</pre>
        </div>
    </template>
    ```

    We can add a `style` tag to remove the default borders and margins, and to style the `legend` tags as we had the headers before.

    **📃 SimpleForm.vue**

    ```html
    <style>
        fieldset {
            border: 0;
            margin: 0;
            padding: 0;
        }

        legend {
            font-size: 28px;
            font-weight: 700;
            margin-top: 20px;
        }
    </style>
    ```

    I’m going to use FireFox this time because it has a very nice accessibility inspector tool.

    Checking the accessibility tab, you can see how now the logical grouping of our form will be understood by screen readers.

    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.1614626791978.jpg?alt=media&token=35437814-4997-46d5-932c-8cd89f8535b4](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.1614626791978.jpg?alt=media&token=35437814-4997-46d5-932c-8cd89f8535b4)

    ---

    ## Do NOT rely on placeholders

    A popular design pattern that emerged a few years ago used the `placeholder` attribute of inputs to describe the type of content that the element was expecting. Sadly this is still sometimes used now-a-days instead of a proper label.

    Placeholders should only be used to describe the intended value, but not as a replacement for a descriptive label. Placeholders disappear whenever a user starts typing into the field, forcing the user to keep in mind what that field was expecting. Additionally, some users can have problems differentiating between a field with a placeholder and a field that has pre-populated or filled content.

    As far as screen readers go, each screen reader may treat the `placeholder` attribute differently, but as long as a correctly set `label` is in place, it shouldn’t be much of a concern to leave it in.

    ---

    ## Labels

    Speaking about labels, let’s talk about a really powerful accessibility feature that is sadly very commonly underused, or misused, in forms.

    If we navigate to FireFox again in the accessibility tab and inspect our **Title** input, we can see a ⚠️ icon right next to it. This means we have a problem.

    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1614626791979.jpg?alt=media&token=51e65953-28fd-4e9e-b301-8bcc303a6309](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1614626791979.jpg?alt=media&token=51e65953-28fd-4e9e-b301-8bcc303a6309)

    Let’s take a look at the information panel. The _Checks_ section is already telling us the issue: “Form elements should have a visible text label”

    This may come as a surprise, since our Title field clearly has a `label` on top of it describing what we intend for this input.

    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1614626797344.jpg?alt=media&token=e6ba5fac-842f-4ee1-89b9-b5f767836afb](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1614626797344.jpg?alt=media&token=e6ba5fac-842f-4ee1-89b9-b5f767836afb)

    For our sighted users, however, this is not evident. We have not yet _linked_ these two HTML elements together, and that is an assumption that a screen reader cannot afford to make. Thankfully this is a very easy fix!

    There are a few ways to link an input element with its label, the first one is to actually nest the input inside of the label element.

    ```html
    <label>
        Title
        <input />
    </label>
    ```

    This is one of the easiest ways to make sure that your input is always correctly linked to the related label, but I want to go into depth into the second and usually more “common” way to relate HTML elements because it’s going to come in handy later when we look at error messages. This method involves using IDs.

    Let’s jump directly into our `BaseInput` component and figure out how to create a relationship between our `<label>` and `<input>` by using an ID.

        You may be thinking that perhaps the most obvious option would be to add a property, so that the parent can determine the `id` of the element, and then we don’t have to worry about it inside our component. And you would be right… But what if there were a way we could dynamically generate unique number identifiers for every component in our form without having to resort to manual props?

        We are going to create a Vue 3 `composable` that allows us to create these dynamic unique identifiers, or UUIDs for short. I know this is a bit of a jump from the pace of the course, but if you need a refresher on Vue 3 composition API or composables we have a course titled [Build a Gmail Clone with Vue 3](https://www.vuemastery.com/courses/build-a-gmail-clone-with-vue3/tour-the-project) here on Vue Mastery to get you up to speed. At any rate, don’t worry too much, it’s going to be a really simple one.

        If you’re following along with the repository files, I’ve gone ahead and created a `UniqueID.js` file inside the `features` folder. Let’s take a look.

        **📃 UniqueID.js**

        ```jsx
        let UUID = 0

        export default function UniqueID () {
        const getID = () => {
        UUID++
        return UUID
        }

        return {
        getID
        }
        }
        ```

        First we declare a `let` variable with a default value of `0`. This will increase as we create more and more components - the first component will have an id of `1`, the second of `2`, and so on.

        We are going to export a function `UniqueID`. When executed, this function will return an object, which contains a function under the property `getID`. This function will increase by 1 the global `UUID` counter and `return` it.

        Know also that there are plenty of UUID libraries out there that you can use in place of this custom solution, but I wanted to show you just how easy it can be.

        Let’s look at this in action to better understand it, by looking at `BaseInput.vue`.

        First, we are going to import our new composable.

        **📃 BaseInput.vue**

        ```html
        <script>
            import UniqueID from '../features/UniqueID'
            export default {
                ...
            }
        </script>
        ```

        Now that we have it ready, we can generate a new unique ID inside our component’s `setup` method. Let’s go ahead and do that.

        **📃 BaseInput.vue**

        ```html
        <script>
            import UniqueID from '../features/UniqueID'
            export default {
                props: {
                    ...
                },
                setup() {
                    const uuid = UniqueID().getID()
                    return {
                        uuid
                    }
                }
            }
        </script>
        ```

        Notice that we are executing both the `UniqueID`composable, and then the `getID` method inside. This will give us a completely unique ID number every time a component is instantiated.

        Finally, we `return` an object with the `uuid` so that we can use it in our template.

        Speaking of which, let’s go back into the template and tie the label and input together.

        To accomplish this, we need to give the `input` element an `id` attribute value. We will bind the `id` to our `uuid`. After we have the `input` setup with its own unique ID, we can now tell the label that its describing the `input` by setting the `for` attribute of the label.

        Note: All of these are vanilla HTML attributes, no crazy Vue magic here other than the ease of binding all of them.

        **📃 BaseInput.vue**

        ```html
        <template>
            <label :for="uuid" v-if="label">{{ label }}</label>
            <input v-bind="$attrs" :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field" :id="uuid">
        </template>
        ```

        Let’s head back to the browser. The first thing I want to point out to you is that the warning sign on our field is gone. And if we check under the bit where it says _relations_ inside the accessible properties, we can see now that it shows a new entry: `labelledby: "Title"`. If you hover over this element you can now see in the browser which element exactly its referring to. Neat, right?

        ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.1614626801772.jpg?alt=media&token=b39f6955-ae65-4fa5-965d-7d19e4dd784c](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.1614626801772.jpg?alt=media&token=b39f6955-ae65-4fa5-965d-7d19e4dd784c)

        Now, if we take a look at the **inspector** tab, and we look at our input elements, we can see that they have automatically been assigned ids 1 and 2, respectively.

        ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.1614626804881.jpg?alt=media&token=07b34356-eeff-4928-97a7-423e8ec49827](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.1614626804881.jpg?alt=media&token=07b34356-eeff-4928-97a7-423e8ec49827)

        We still need to add a `uuid` to our Checkbox, Radio, and Select components. Are you up for a challenge? Try doing this bit yourself. It will be as straightforward as replicating exactly what we just did here with `BaseInput`.

        ---

        ## Accessible errors

        Have you ever filled out a form just to hit the submit button and nothing seemed to work? It was clearly not submitting, and there was no visible error anywhere, yet _something_ was clearly wrong. This situation is not foreign to most Internet users, but imagine the exasperation when you require accessible tools and the form doesn’t easily tell you what’s wrong with your inputs.

        Let’s first go into our `BaseInput.vue` component and add a new prop, `error`, that will allow us to set a String with an error message in case the component is in an error state.

        **📃 BaseInput.vue**

        ```jsx
        props: {
        label: {
        type: String,
        default: ''
        },
        modelValue: {
        type: [String, Number],
        default: ''
        },
        error: {
        type: String,
        default: ''
        }
        },
        ```

        We will display this error below our `input` field whenever an error is present, so if the `error` property is set to anything other than an empty string.

        **📃 BaseInput.vue**

        ```html
        <template>
            <label :for="uuid" v-if="label">{{ label }}</label>
            <input v-bind="$attrs" :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field" :id="uuid">
            <p v-if="error" class="errorMessage">
                {{ error }}
            </p>
        </template>
        ```

        We will also go back to `SimpleForm.vue` and add an `error` message to our “Title” input, so that we can see how it behaves on the browser. Note that we also added a class of `errorMessage` that will simply color it red.

        **📃 SimpleForm.vue**

        ```html
        <BaseInput v-model="event.title" label="Title" type="text" error="This input has an error!" />
        ```

        Now let’s look at the browser, the error is correctly displaying under the title once the `error` prop is set. Notice that the “Description” input which is also a `BaseInput` is displaying no error because the prop `error` is not set to anything.

        ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F6.1614626807929.jpg?alt=media&token=1a830dea-1959-4c7e-babf-62de41cc3f0e](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F6.1614626807929.jpg?alt=media&token=1a830dea-1959-4c7e-babf-62de41cc3f0e)

        If we open our Accessibility tab in Firefox once again and inspect the `input` element, we can see that there is nothing tying the error to the actual title input. This is where most forms fall short. Just because the error message is “near” the input doesn’t mean that a screen reader will be able to identify it as part of the error.

        Luckily there is a straightforward solution to this problem: the `aria-describedby` attribute. This attribute allows us to declare directly on the `input` element which other elements describe it.

        The attribute can take a string list of IDs for other HTML elements in the page, so first we’re going to add a unique ID to our label. Luckily, we already have a UUID number associated with the instance of the component to do it.

        Let’s head back to `BaseInput` and add the `id` binding to the error `p` tag.

        **📃 BaseInput.vue**

        ```html
        <p v-if="error" class="errorMessage" :id="`${uuid}-error`">
            {{ error }}
        </p>
        ```

        Notice that we are appending the `-error` string to the UUID. We need this identifier to be unique, and the UUID by itself is already in use by the `input`.

        Now that our error message has a unique ID, we can set it as a “description” for the input element with the `aria-describedby` attribute.

        **📃 BaseInput.vue**

        ```html
        <template>
            <label :for="uuid" v-if="label">{{ label }}</label>
            <input v-bind="$attrs" :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field" :id="uuid" :aria-describedby="error ? `${uuid}-error` : null">
            <p v-if="error" class="errorMessage" :id="`${uuid}-error`">
                {{ error }}
            </p>
        </template>
        ```

        Notice that we’re adding a secondary check to see if `error` is set. This allows us a bit of relationship hygiene between our elements — when no error is present, no error message will be displayed and the id will point to nothing.

        If we check our browser again, we can see that when the error is present, a new `described by` entry is present in the input’s relationship object.

        ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F7.1614626807930.jpg?alt=media&token=f61ca57d-d701-48e8-89df-3bbb4d67a571](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F7.1614626807930.jpg?alt=media&token=f61ca57d-d701-48e8-89df-3bbb4d67a571)

        One more thing though… Because we are using `v-if` to display this information on and off, we want to make sure that screen readers announce/read it whenever it becomes displayed. To do this, we’re going to add an attribute of `aria-live="assertive`. Another way would be to add a `role` attribute of “alert”, but I’ve found that the aria-live tends to work better with a variety of screen readers.

        **📃 BaseInput.vue**

        ```html
        <p v-if="error" class="errorMessage" :id="`${uuid}-error`" aria-live="assertive">
            {{ error }}
        </p>
        ```

        ---

        ## Explicit input state

        Another thing we can quickly add to our input to make it even more accessible is the `aria-invalid` attribute. A mistake that I’ve seen many forms make is to try and rely on a red border around an invalid input. For obvious reasons, this is not accessible.

        We’ve already taken steps into accessible errors, but let’s make sure to also notify screen readers on the invalid state of an input to provide better feedback for our users.

        We are going to add the `aria-invalid` attribute to our input, and toggle it off and on depending on whether the `error` prop is set. When the input is valid, `null` will make it so that the attribute is not added to the input element.

        **📃 BaseInput.vue**

        ```html
        <input v-bind="$attrs" :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field" :id="uuid" :aria-describedby="error ? `${uuid}-error` : null" :aria-invalid="error ? true : null">
        ```

        If we go back to the browser and inspect the input using the Accessibility tool on Firefox, we can see that the state of “invalid” has now been added to the active states of the input.

        ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F8.1614626812519.jpg?alt=media&token=54ba744a-747d-46df-8893-015debb896c1](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F8.1614626812519.jpg?alt=media&token=54ba744a-747d-46df-8893-015debb896c1)

        Other noteworthy states that we could also add attributes for are `readonly`, `disabled` and `required`. These three can be set directly with HTML5 attributes of the same name, or with their aria counterparts: `aria-readonly`, `aria-disabled`, and `aria-required`.

        ---

        ## Don’t disable the submit button

        If a form is not valid, then it makes sense to set the `disabled` attribute to true on the submit button so that the user can’t submit the form, right? We can even style the button with a different color to convey that it won’t be clickable.

        There’s a big problem with this though. Users that rely on screen readers will not get any feedback at all, the button will be completely ignored by the screen reader when tabbing through the form. This clearly can be very confusing and frustrating.

        I suggest instead that you make any and all checks to make sure your form is valid before submitting it on the `sendForm` method that we created on the `SimpleForm` component. If everything checks out, we submit the form normally.

        If something is wrong, then set the necessary errors in your form with the tools that we just learned to notify the user that something is wrong.

        ---

        ## Wrapping up

        As you can see, with a few quick lines of HTML and some strategically placed props, we managed to turn around our `BaseInput` component into something a lot more accessible.

        I do want to stress though, that as far as the topic of accessibility goes, this only begins to scratch the surface. But with these few tips, you should be able to set the course for a more inclusive and accessible form for your projects!