HTML and CSS course Β· Module 10: Accessibility (a11y)

Accessible Forms

6 min read
In this lesson7

Imagine filling in a booking form with your eyes closed while the screen reader says only "edit field, blank" at every field. You do not know whether to type your name, your email or the date of your visit. That is what a form without labels sounds like. Forms are among the most important interactive elements on websites. Just as scribes in Ancient Egypt had to fill in precise papyrus documents, users must be guided through forms in a clear and intuitive way - especially those who use assistive technologies.

Labels

Every form field must have an associated label. This is the absolute foundation of form accessibility - a label is the inscription on the seal that says what needs to be entered.

Explicit Association

The most reliable way is to connect the label's for attribute with the field's id attribute. The values must be identical, and the id must be unique on the whole page:

1<!-- Best method - for/id -->
2<label for="name">Full name:</label>
3<input type="text" id="name" name="name">
4
5<!-- Screen reader will say: "Full name, text field" -->

The reader will announce the label together with the field type, and clicking the label puts the cursor in the field - a larger click target helps everyone, especially on a phone. The field itself did not change its look or behavior, it only gained a name.

Implicit Association

The second way is to place the field inside the <label> element - then the for and id attributes are not needed:

1<!-- Input inside label -->
2<label>
3    Email address:
4    <input type="email" name="email">
5</label>

This association is also correct, but as the default I recommend explicit for and id: it works no matter how you arrange the elements in HTML, and it is easy to check in DevTools.

What NEVER to Do

You can see two mistakes on many websites, even though both look completely harmless:

1<!-- BAD - no label -->
2<input type="text" placeholder="Enter name">
3<!-- Placeholder DISAPPEARS after typing and is NOT a label! -->
4
5<!-- BAD - text next to it but not associated -->
6<span>Name:</span>
7<input type="text">
8<!-- Reader does not know that "Name:" is the label for this field -->

A placeholder disappears with the first letter, so the user loses the hint halfway through, and the text in a <span> is just a caption to the reader, connected to nothing.

Fieldset and Legend

Group related fields using <fieldset> and <legend>. The legend is the title of the whole group, which the reader attaches to the fields inside:

1<fieldset>
2    <legend>Personal Information</legend>
3
4    <label for="first-name">First name:</label>
5    <input type="text" id="first-name" name="firstName">
6
7    <label for="last-name">Last name:</label>
8    <input type="text" id="last-name" name="lastName">
9</fieldset>
10
11<fieldset>
12    <legend>Choose an exhibition:</legend>
13
14    <label>
15        <input type="radio" name="exhibition" value="pharaohs">
16        Treasures of the Pharaohs
17    </label>
18
19    <label>
20        <input type="radio" name="exhibition" value="mummies">
21        Royal Mummies
22    </label>
23
24    <label>
25        <input type="radio" name="exhibition" value="jewelry">
26        Ancient Egyptian Jewelry
27    </label>
28</fieldset>

<fieldset> is especially important for groups of radio buttons and checkboxes - without it, the reader does not know the context of the question. The user would hear "Treasures of the Pharaohs, radio button" without knowing that it answers a question about the exhibition. The shared name attribute, in turn, makes sure that only one option can be selected.

Error Messages

Error handling must be accessible - the user must know that an error has occurred and how to fix it.

Inline Errors

You connect a message to its field with three ARIA attributes that you already know from the ARIA lesson:

1<div class="form-group">
2    <label for="email">Email address (required):</label>
3    <input type="email" id="email" name="email"
4           aria-required="true"
5           aria-invalid="true"
6           aria-describedby="email-error">
7    <p id="email-error" class="error" role="alert">
8        Please enter a valid email address, e.g., john@example.com
9    </p>
10</div>

aria-invalid="true" marks the field as invalid, and aria-describedby points to the message's id, so the reader reads it right after the label. role="alert" makes the message announced immediately when it appears on the page. Notice the wording: it says how to fix the error, not just that something is wrong.

Error Summary

In a longer form, add a summary at the top in which every error is a link leading to its field:

1<div role="alert" aria-labelledby="error-summary-title">
2    <h2 id="error-summary-title">
3        The form contains 2 errors:
4    </h2>
5    <ul>
6        <li>
7            <a href="#email">Email field - enter a valid address</a>
8        </li>
9        <li>
10            <a href="#phone">Phone field - minimum 9 digits required</a>
11        </li>
12    </ul>
13</div>

Clicking the #email link takes the user straight to the invalid field, so they do not have to search for it with Tab across the whole form.

Hints and Instructions

Show the requirements for a field before the user makes a mistake. The same aria-describedby connects the field to its instructions:

1<label for="password">Password:</label>
2<input type="password" id="password" name="password"
3       aria-describedby="password-requirements">
4<div id="password-requirements">
5    <p>Password must contain:</p>
6    <ul>
7        <li>At least 8 characters</li>
8        <li>An uppercase letter</li>
9        <li>A number</li>
10        <li>A special character</li>
11    </ul>
12</div>

The reader reads the requirements as soon as focus lands in the password field, so a sighted person and a blind person get the same information at the same moment.

Autocomplete

The autocomplete attribute helps browsers and assistive technologies fill in the form automatically:

1<form>
2    <label for="full-name">Full name:</label>
3    <input type="text" id="full-name" name="name"
4           autocomplete="name">
5
6    <label for="user-email">Email:</label>
7    <input type="email" id="user-email" name="email"
8           autocomplete="email">
9
10    <label for="user-tel">Phone:</label>
11    <input type="tel" id="user-tel" name="phone"
12           autocomplete="tel">
13</form>

Values such as name, email and tel come from the HTML standard. WCAG 2.1 even has a separate success criterion (1.3.5, level AA) that requires fields collecting user data to be marked this way - people with motor disabilities save a lot of typing thanks to it.

Complete Form Example

Finally, all the elements together - a form for booking a museum visit:

1<form aria-labelledby="form-title" novalidate>
2    <h2 id="form-title">Book a Museum Visit</h2>
3
4    <fieldset>
5        <legend>Personal Information</legend>
6
7        <div class="form-group">
8            <label for="visitor-name">Full name:</label>
9            <input type="text" id="visitor-name"
10                   autocomplete="name"
11                   aria-required="true">
12        </div>
13
14        <div class="form-group">
15            <label for="visitor-email">Email:</label>
16            <input type="email" id="visitor-email"
17                   autocomplete="email"
18                   aria-required="true"
19                   aria-describedby="email-hint">
20            <p id="email-hint" class="hint">
21                We will send the confirmation to this address.
22            </p>
23        </div>
24    </fieldset>
25
26    <fieldset>
27        <legend>Visit Details</legend>
28
29        <div class="form-group">
30            <label for="visit-date">Visit date:</label>
31            <input type="date" id="visit-date"
32                   aria-required="true">
33        </div>
34
35        <div class="form-group">
36            <label for="visitors-count">Number of people:</label>
37            <input type="number" id="visitors-count"
38                   min="1" max="20" value="1">
39        </div>
40    </fieldset>
41
42    <button type="submit">Book a visit</button>
43</form>

Notice novalidate on the <form>: it turns off the browser's built-in validation bubbles, because the page's own script checks the fields, and aria-required="true" tells the reader which fields are required. If you are not writing your own validation, simply use the required attribute - the browser will block submitting an empty field, and the reader will still announce it as required.

Accessible forms are like a well-designed papyrus document - clear instructions, readable fields and immediate feedback help every user complete them successfully. In the lesson on testing you will check such a form with a screen reader.

Practice

In the editor below you will find a booking form. Go through it with Tab and check that every field has a label and the group of options has its legend:

Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Accessible forms</title>
7    <link rel="stylesheet" href="style.css">
8</head>
9<body>
10    <h1>Museum visit booking form</h1>
11
12    <form aria-labelledby="form-title" novalidate>
13        <h2 id="form-title">Book a visit</h2>
14
15        <fieldset>
16            <legend>Personal information</legend>
17
18            <div class="form-group">
19                <label for="visitor-name">
20                    Full name:
21                    <span class="required" aria-hidden="true">*</span>
22                </label>
23                <input type="text" id="visitor-name"
24                       name="name"
25                       autocomplete="name"
26                       aria-required="true">
27            </div>
28
29            <div class="form-group">
30                <label for="visitor-email">
31                    Email address:
32                    <span class="required" aria-hidden="true">*</span>
33                </label>
34                <input type="email" id="visitor-email"
35                       name="email"
36                       autocomplete="email"
37                       aria-required="true"
38                       aria-describedby="email-hint">
39                <p id="email-hint" class="hint">
40                    We will send the booking confirmation to this address.
41                </p>
42            </div>
43
44            <div class="form-group">
45                <label for="visitor-tel">Phone:</label>
46                <input type="tel" id="visitor-tel"
47                       name="phone"
48                       autocomplete="tel">
49            </div>
50        </fieldset>
51
52        <fieldset>
53            <legend>Choose an exhibition</legend>
54
55            <label class="radio-label">
56                <input type="radio" name="exhibition"
57                       value="tutankhamun">
58                Treasures of Tutankhamun
59            </label>
60
61            <label class="radio-label">
62                <input type="radio" name="exhibition"
63                       value="mummies">
64                Royal mummies
65            </label>
66
67            <label class="radio-label">
68                <input type="radio" name="exhibition"
69                       value="jewelry">
70                Ancient Egyptian jewelry
71            </label>
72        </fieldset>
73
74        <fieldset>
75            <legend>Visit details</legend>
76
77            <div class="form-group">
78                <label for="visit-date">
79                    Visit date:
80                    <span class="required" aria-hidden="true">*</span>
81                </label>
82                <input type="date" id="visit-date"
83                       name="date"
84                       aria-required="true">
85            </div>
86
87            <div class="form-group">
88                <label for="visitors-count">Number of people:</label>
89                <input type="number" id="visitors-count"
90                       name="count"
91                       min="1" max="20" value="1">
92            </div>
93
94            <div class="form-group">
95                <label>
96                    <input type="checkbox" name="guide">
97                    I want a guided tour of the exhibition
98                </label>
99            </div>
100        </fieldset>
101
102        <p class="required-info">
103            <span class="required" aria-hidden="true">*</span>
104            Required fields
105        </p>
106
107        <button type="submit">Book a visit</button>
108    </form>
109</body>
110</html>

Remember: every field is a seal on a papyrus - without the inscription in its <label>, nobody knows what to press into it.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. When should you use <fieldset> and <legend> elements in forms?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange the correct association of a label with an input:

  • Code editor

    Create an accessible reservation form with fieldset/legend, label for/id, aria-required, and aria-describedby. Add a radio button group with fieldset/legend.

  • Click in order

    Arrange the correct fieldset structure with legend for a radio group:

Useful articles