In 04-01 we saw that native form controls (<input>, <select>, <textarea>, <button>) come with role, focus and keyboard operation built in. But an accessible form needs more than good controls: it needs every control to have a name, related fields to be grouped, hints and errors to reach everyone, and the focus order to be logical. Those are exactly the 3.3.x criteria ("Input Assistance") that in Module 3 we studied as a requirement and now implement.
Our practice ground is Cursalia's enrollment/registration form: name, email, phone, password, level of education, interests and terms acceptance. We'll build it piece by piece, always with correct versus incorrect examples, thinking about Marta (NVDA must read her each field's label), Hugo (needs clear, unambiguous instructions) and Lucía (traverses it entirely with the keyboard).
Contents
- The label: associating
labeland control - Why the placeholder is not a label
- Grouping with
fieldsetandlegend - Required fields:
requiredandaria-required - Instructions and hints with
aria-describedby - Accessible errors (and a first look at
role="alert") autocompleteandinputtypes- Focus and logical order
- Common mistakes and tips
- Exercises
- Conclusion
The label: associating label and control
Every form control must have an accessible label. The label is the control's name (remember 4.1.2): without it, Marta hears "edit box" without knowing what to type. There are two correct ways to associate a <label> with its control.
Option A — for / id (the most common and flexible): the label's for attribute points to the control's id.
<!-- CORRECT: for points to the id -->
<label for="email">Email</label>
<input type="email" id="email" name="email">Option B — wrapping label: the control goes inside the <label>. No for/id needed.
Both make clicking the text focus the field (a larger click area, something Lucía appreciates) and make the reader announce it with its name. The usual mistake is not associating them: loose text next to an input.
<!-- INCORRECT: the text is not associated with the field -->
<span>Email</span>
<input type="email" name="email">Here there is no <label> (or its for doesn't match any id): Marta hears "blank edit box". If by design you can't show visible text (for example, the catalog search box with just a magnifying glass), use aria-label, which we'll cover in 04-03:
Why the placeholder is not a label
It's tempting to add just a placeholder="Email" and skip the <label>. That's a serious mistake for several reasons:
| Problem with the placeholder as a label | Who it affects |
|---|---|
| Disappears when you start typing; you no longer know what the field was | Hugo (memory/attention), everyone |
| Its contrast is usually low (light gray) and fails 1.4.3 | Sofía (low vision) |
| Many readers don't announce it as a reliable name | Marta |
| You can't click it to focus the field | Lucía |
<!-- INCORRECT: placeholder only, no label -->
<input type="email" placeholder="Email">
<!-- CORRECT: visible label; the placeholder, if any, only as a format example -->
<label for="email">Email</label>
<input type="email" id="email" placeholder="[email protected]">Rule: the placeholder is, at most, a format hint; it never replaces the label.
Grouping with fieldset and legend
When several controls form a single conceptual group —a set of radios (pick one) or checkboxes (pick several)—, you must wrap them in a <fieldset> and give them a common title with <legend>. That way the reader announces the group ("Level of education, group") before each option, giving the context a lone radio wouldn't have.
<!-- CORRECT: radio group with fieldset/legend -->
<fieldset>
<legend>Level of education</legend>
<label><input type="radio" name="level" value="secondary"> Secondary</label>
<label><input type="radio" name="level" value="degree"> University degree</label>
<label><input type="radio" name="level" value="master"> Master's or higher</label>
</fieldset>Marta hears: "Level of education, group. Secondary, radio button, 1 of 3". Without the fieldset, she'd hear three lone radios without knowing which question they answer.
<!-- INCORRECT: loose text groups nothing semantically -->
<p>Level of education</p>
<label><input type="radio" name="level" value="degree"> Degree</label>The same applies to interests (checkboxes) or accepting several terms. Don't overuse fieldset for independent fields (name, email): it's only for groups.
Required fields: required and aria-required
Marking a field as required has two sides you must cover at once: the functional and the communicative.
required: native attribute. Activates the browser's validation and exposes the "required" state to the reader (Marta hears "required"). It's the first choice.aria-required="true": only if you need to communicate the requirement without triggering the browser's native validation. Don't duplicate both without reason;requiredis usually enough.- Visual and textual indication: a red asterisk isn't enough (fails 1.4.1, "not by color/shape alone"). Explain what the asterisk means and, better still, reinforce it with text.
<!-- CORRECT: required communicated three ways: native, visual and textual -->
<p id="required-note">Fields marked with * are required.</p>
<label for="name">Full name *</label>
<input type="text" id="name" name="name" required
aria-describedby="required-note"><!-- INCORRECT: the requirement lives only in the color of the asterisk -->
<label for="name">Full name <span style="color:red">*</span></label>
<input type="text" id="name"> <!-- no required, no explanation of the * -->Instructions and hints with aria-describedby
Many fields need a hint: password format, what the phone number is used for, etc. For that hint to be read alongside the field (and not left as orphan text beside it), you associate it with aria-describedby, which points to the id of the hint text. The reader reads the label first and the description afterward.
<!-- CORRECT: the hint is associated with aria-describedby -->
<label for="pass">Password</label>
<input type="password" id="pass" name="pass"
aria-describedby="pass-hint" required>
<p id="pass-hint">At least 8 characters, with one letter and one number.</p>Marta hears: "Password, edit box, required. At least 8 characters, with one letter and one number". Hugo reads that same clear instruction before typing, not after failing. aria-describedby can point to several ids separated by spaces (for example, the hint and the field's error message).
<!-- INCORRECT: the hint exists visually but is not associated -->
<label for="pass">Password</label>
<input type="password" id="pass">
<p>At least 8 characters, with one letter and one number.</p> <!-- no aria-describedby: the reader doesn't link it -->Accessible errors (and a first look at role="alert")
When the user makes a mistake, criterion 3.3.1 requires identifying the error in text (not by color alone) and describing it usefully. Best practices:
- Explicit text, not just a red border: "The email is invalid: the @ is missing".
- Associated with the field using
aria-describedby, so the reader reads it when focusing that field. - Don't rely on color: add an icon or the word "Error" (this is 1.4.1, which we'll cover in depth in 04-04).
- Moving focus to the first field with an error or summarizing the errors at the top helps Lucía and Marta.
<!-- CORRECT: error in text, associated, and not by color alone -->
<label for="email">Email *</label>
<input type="email" id="email" name="email" required
aria-describedby="email-err" aria-invalid="true">
<p id="email-err" class="error">
<span aria-hidden="true">⚠️</span> Error: the email must include an @.
</p>Here aria-invalid="true" marks the field as invalid in the accessibility tree, and aria-describedby makes the message read when you focus it.
When the error appears dynamically (after clicking "Submit", without reloading the page), you also need to announce it even if focus is elsewhere. That's what live regions are for. A brief introduction: a container with role="alert" (or aria-live="assertive") makes the reader read automatically any text that appears inside it, without the user having to go looking for it.
<!-- The reader will announce the text as soon as your JS inserts it here -->
<div role="alert" id="error-summary"></div>// On validation, you insert the message and NVDA reads it on its own:
document.getElementById('error-summary').textContent =
'Check the form: there are 2 fields with errors.';We stop at this introduction: the detail of aria-live, role="status" vs role="alert" and dynamic regions is covered at the markup level in 04-03, and full validation with JavaScript (when to validate, how to move focus, updating messages live) belongs to Module 5.
autocomplete and input types
Choosing the right type and adding autocomplete reduces everyone's effort, and very noticeably for Lucía (less typing), Hugo (less to remember) and anyone using voice control or a phone.
| Field | Correct type |
autocomplete |
Benefit |
|---|---|---|---|
email |
email |
Keyboard with @, format validation, autofill |
|
| Phone | tel |
tel |
Numeric keypad on mobile |
| Name | text |
name |
Autofill of the saved name |
| Password (registration) | password |
new-password |
The manager suggests a secure one |
<!-- CORRECT: types and autocomplete that help both the browser and the person -->
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email">
<label for="tel">Phone</label>
<input type="tel" id="tel" name="tel" autocomplete="tel">autocomplete also meets criterion 1.3.5 ("Identify Input Purpose"), which helps tools that adapt or fill in fields for the user.
Focus and logical order
The order in which Lucía reaches the fields when pressing Tab must follow the form's visual and logical order. The key is that the DOM order matches the visual order: if you lay out top to bottom in the HTML, focus will flow correctly with no tricks.
- Don't use a positive
tabindex(tabindex="1","2"...): it breaks the natural order and is a constant source of bugs. Let the DOM dictate the order. - A real
<button type="submit">lets you submit the form with Enter from any field. - After submitting with errors, moving focus to the error summary or the first failing field saves Lucía from tabbing blindly (JS implementation: Module 5).
<!-- INCORRECT: positive tabindex forces an artificial, fragile order -->
<input id="a" tabindex="3">
<input id="b" tabindex="1">
<input id="c" tabindex="2"><!-- CORRECT: no tabindex; the DOM order is already the focus order -->
<input id="a">
<input id="b">
<input id="c">Common Mistakes and Tips
- Fields with no associated
<label>. This is the number-one failure. Every control needs a name viafor/id, a wrapping label or, as a last resort,aria-label. - Using the placeholder as a label. It disappears, contrasts poorly and is unreliable as a name. Always add a visible label.
- Requirement by color only. The red asterisk isn't enough: add
requiredand explain what the*means. - Hints and errors left unassociated. A hint next to a field that isn't linked with
aria-describedbyis invisible to the reader. - Errors by red border only. Marta doesn't see color: the error must be in text and be associated.
- Radios/checkboxes without
fieldset/legend. They lose the context of the question. - Positive
tabindex. Almost never the solution; order the DOM instead. - Tip: fill in your entire form using only the keyboard and a reader, with your eyes closed. If you can't tell what a field asks for or where the error is, it isn't accessible yet.
Exercises
Exercise 1: give the fields a name
This fragment of the Cursalia registration doesn't associate labels. Fix it using for/id and replace the placeholder-label with a real label.
Exercise 2: group the interests
Users can check several interests. Mark up this checkbox group correctly so the reader announces the group title.
<p>Interests</p>
<label><input type="checkbox" name="int" value="dev"> Development</label>
<label><input type="checkbox" name="int" value="ux"> UX Design</label>
<label><input type="checkbox" name="int" value="data"> Data</label>Exercise 3: a field with accessible hint and error
Write the "Password" field: visible label, required, with an associated format hint and an error message (associated and not dependent on color alone) for when the password is too short.
Solutions
Solution 1. Each control gets an associated label; the placeholder gives way to a real <label>:
<label for="name">Name</label>
<input type="text" id="name" name="name" autocomplete="name">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email">Solution 2. The whole group goes in a <fieldset> with a <legend>:
<fieldset>
<legend>Interests</legend>
<label><input type="checkbox" name="int" value="dev"> Development</label>
<label><input type="checkbox" name="int" value="ux"> UX Design</label>
<label><input type="checkbox" name="int" value="data"> Data</label>
</fieldset>Now the reader announces "Interests, group" before each box.
Solution 3. Visible label + required + hint with aria-describedby + associated error in text, with an icon in addition to color:
<label for="pass">Password *</label>
<input type="password" id="pass" name="pass" required
autocomplete="new-password"
aria-describedby="pass-hint pass-err"
aria-invalid="true">
<p id="pass-hint">At least 8 characters, with one letter and one number.</p>
<p id="pass-err" class="error">
<span aria-hidden="true">⚠️</span> Error: the password is too short.
</p>Note that aria-describedby points to two ids (hint and error), so the reader reads both. When the password is valid, your code will remove aria-invalid and the error message (that's already JS logic, Module 5).
Conclusion
An accessible form isn't magic: it's labeling every control, grouping what belongs together, communicating the requirement and hints through associated text, describing errors clearly and not by color alone, and respecting the DOM's focus order. With this, Cursalia's enrollment form stops being a wall for Marta, Hugo and Lucía. We've leaned almost entirely on native HTML and only peeked at aria-describedby, aria-invalid and role="alert".
Those aria-* attributes are the door to the next lesson. In 04-03 · Using ARIA Roles and Properties we'll step back to understand what ARIA is, its five golden rules (starting with "don't use it if there's native HTML"), how the accessible name we've been using is computed (aria-label, aria-labelledby, aria-describedby) and how to describe states like aria-expanded on Cursalia's filters dropdown, always at the markup level.
Web Accessibility Course
Module 1: Introduction to Web Accessibility
- What Is Web Accessibility?
- The Importance of Web Accessibility
- Overview of Accessibility Laws and Standards
- Introduction to WCAG
Module 2: Understanding Disabilities and Assistive Technologies
Module 3: Principles of Accessible Design
- Perceivable: Making Content Available to the Senses
- Operable: User Interface and Navigation
- Understandable: Information and Operation
- Robust: Compatibility with Current and Future Technologies
Module 4: Implementing Accessibility in HTML and CSS
Module 5: Accessibility in JavaScript and Multimedia
- Creating Accessible JavaScript Widgets
- Keyboard Accessibility
- Accessible Video and Audio Content
- Providing Text Alternatives for Images
Module 6: Accessibility Testing and Evaluation
- Manual Testing Techniques
- Automated Testing Tools
- User Testing with Assistive Technologies
- Interpreting Accessibility Reports
