Placing non-interactive content between form controls
Screen reader interaction with forms usually happens in focus mode. So if there are any non-interactive elements (like a paragraph) in the form, they are prone to be missed. To prevent this, they need to be attached specifically to the form controls. There are several ways to achieve this goal.
Screen reader users generally interact with form controls using focus mode, meaning they navigate back and forth only between interactive (which are focusable) elements using the Tab key (if you haven't done this yet, go back and read Screen readers' browse and focus modes).
Especially in complex forms, it can be necessary to have non-interactive content next to some control: for example paragraphs (or a list) presenting terms and conditions next to an "I agree" checkbox. As this content is not focusable, it is prone to be completely missed by desktop screen reader users when tabbing through the form.
Alas, such content needs to be specifically attached to the form controls, so it is recognisable also in focus mode. There are various approaches to achieve this goal.
Best approach: ARIA
While we usually do not favour solutions using ARIA over traditional HTML, regarding forms, in our experience ARIA is the most robust way to associate content to controls (if you haven't done this yet, go back and read ARIA - when HTML simply is not enough).
It is easy to attach a little descriptive text to any form control using aria-describedby:
<form><divclass="control"><labelfor="full_name">Full name</label><inputaria-describedby="full_name_description"id="full_name"type="text" /><pclass="description"id="full_name_description">
Please tell us your name.
</p></div><divclass="control"><labelfor="biography">Biography</label><textareaaria-describedby="biography_description"id="biography"></textarea><pclass="description"id="biography_description">
Please tell us something about your history.
</p></div><fieldset><legend>Gender</legend><divclass="control"><inputaria-describedby="gender_male_description"id="gender_male"name="gender"type="radio" /><labelfor="gender_male">Male</label><pclass="description"id="gender_male_description">
You may be this if you like beer, gadgets, and Playboy Magazine.
</p></div><divclass="control"><inputaria-describedby="gender_female_description"id="gender_female"name="gender"type="radio" /><labelfor="gender_female">Female</label><pclass="description"id="gender_female_description">
You may be this if you like prosecco, make up, and Playgirl Magazine.
</p></div></fieldset><divclass="control"><labelfor="age_group">Age Group</label><selectaria-describedby="age_group_description"id="age_group"><option>
0 to 9 Years
</option><option>
10 to 19 Years
</option><option>
20 to 29 Years
</option><option>
30 to 39 Years
</option><option>
40 to 49 Years
</option><option>
50 to 59 Years
</option><option>
60 to 69 Years
</option><option>
70 to 79 Years
</option><option>
80 to 89 Years
</option><option>
90 to 99 Years
</option></select><pclass="description"id="age_group_description">
Tell us how old you are.
</p></div><divclass="control"><inputaria-describedby="accept_agbs_description"id="accept_agbs"type="checkbox" /><labelfor="accept_agbs">I accept the following terms and conditions:</label><ulid="accept_agbs_description"><li>
I entered all data truthfully
</li><li>
I'm a good person
</li><li>
I won't vote for Donald Trump next time
</li></ul></div></form>
You can use aria-describedby on structured text (as you see in the example above, where a list is referenced to the terms and conditions checkbox). Please do not overdo this, because when focusing a form control, screen readers announce all referenced content in one go. And when long contents are referenced, this can quickly become overstraining.
Too much referenced content
Take a look at the following example which clearly overdoes the usage of aria-describedby:
<h1>
A seemingly very important form
</h1><form><divclass="control"><labelfor="full_name">Full name</label><inputaria-describedby="full_name_description"id="full_name"type="text" /><divclass="description"id="full_name_description"><p>
Please tell us your name.
</p><p>
A name typically consists of a first and a last name.
</p><p>
Some people also have a middle name. Or more than one.
</p><p>
Because of that, we offer a single input here, instead of one for each kind of name.
</p><p>
Some cultures even have habits of naming that are very different to ours, so it's really hard (if not impossible) to define a offer an input mask that's working for all. This is another reason why having a single input works fine here.
</p></div></div><divclass="control"><labelfor="biography">Biography</label><textareaaria-describedby="biography_description"id="biography"></textarea><divclass="description"id="biography_description"><p>
Please tell us something about your history.
</p><p>
If you don't know what a "biography" is, please visit the <ahref="https://en.wikipedia.org/wiki/Biography">Wikipedia page about "Biography"</a>.
</p></div></div><divclass="control"><inputaria-describedby="accept_agbs_description"id="accept_agbs"type="checkbox" /><labelfor="accept_agbs">I accept the terms and conditions</label></div><inputtype="submit"value="Submit" /><divid="accept_agbs_description"><h2>
Terms and conditions
</h2><h3>
Definitions
</h3><p>
Some definitions that nobody ever will read.
</p><h3>
Scope of Our Service
</h3><p>
More information that nobody ever will read.
</p><ul><li>
Very important note
</li><li>
Another important note
</li><li>
Maybe less important note
</li><li>
Again, very important note
</li></ul><h3>
Correspondence and Communication
</h3><p>
Still: nobody will ever read this information.
</p><p>
But at least let's put a link to Google here: <ahref="https://www.google.ch/search?q=what+are+terms+and+conditions">What are terms and conditions?</a></p><h3>
Disclaimer
</h3><p>
Yeah, you know what I mean: never-ever-read-information here.
</p><h3>
Etc.
</h3><p>
Etc. etc. etc.
</p></div></form>
Admittedly, this example feels a bit artificial. But you get the point: listening to too much information at a time is making tired quickly. To fully understand, the user will have to manually trigger the whole announcement again and again - a very tedious task, especially for people with mental disabilities.
In addition, a lot of semantic information about the referenced elements (that would be announced in browse mode) is not announced this way: while list items are still recognisable by the prefix "bullet", other semantic information is not announced at all (for examples occurrences of headings and links).
Giving a short clue instead
Instead of referencing all the information, simply give the user a short clue that there is more information somewhere (below a form control, at the end of the page, or wherever):
<h1>
A seemingly very important form
</h1><form><divclass="control"><labelfor="full_name">Full name <spanclass="visually-hidden">For additional info read below</span></label><inputid="full_name"type="text" /><divclass="description"><p>
Please tell us your name.
</p><p>
A name typically consists of a first and a last name.
</p><p>
Some people also have a middle name. Or more than one.
</p><p>
Because of that, we offer a single input here, instead of one for each kind of name.
</p><p>
Some cultures even have habits of naming that are very different to ours, so it's really hard (if not impossible) to define a offer an input mask that's working for all. This is another reason why having a single input works fine here.
</p></div></div><divclass="control"><labelfor="biography">Biography</label><textareaaria-describedby="biography_description"id="biography"></textarea><divclass="description"><p>
Please tell us something about your history.
</p><p>
If you don't know what a "biography" is, please visit the <ahref="https://en.wikipedia.org/wiki/Biography">Wikipedia page about "Biography"</a>.
</p></div></div><divclass="control"><inputid="accept_agbs"type="checkbox" /><labelfor="accept_agbs">I accept the terms and conditions (available at the bottom of this page)</label></div><inputtype="submit"value="Submit" /><divid="biography_description">
For additional info read below
</div><divid="accept_agbs_description"><h2>
Terms and conditions
</h2><h3>
Definitions
</h3><p>
Some definitions that nobody ever will read.
</p><h3>
Scope of Our Service
</h3><p>
More information that nobody ever will read.
</p><ul><li>
Very important note
</li><li>
Another important note
</li><li>
Maybe less important note
</li><li>
Again, very important note
</li></ul><h3>
Correspondence and Communication
</h3><p>
Still: nobody will ever read this information.
</p><p>
But at least let's put a link to Google here: <ahref="https://www.google.ch/search?q=what+are+terms+and+conditions">What are terms and conditions?</a></p><h3>
Disclaimer
</h3><p>
Yeah, you know what I mean: never-ever-read-information here.
</p><h3>
Etc.
</h3><p>
Etc. etc. etc.
</p></div></form>
For "Terms and conditions", the label text was changed so it gives a clue itself (making it "self speaking"). This does not only serve screen reader users, but also visual users who could miss the terms and conditions because of not scrolling enough vertically.
In general, when doing something like that, be sure the referenced content is easily discoverable by the user.
Less good approaches
The following approaches show solutions that are problematic in some way or another for different users. We highly discourage from following them.
Making content focusable
When navigating using the Tab key, the easiest way to prevent non-focusable content from being skipped may seem to simply make it focusable: add a tabindex="0" to a heading, paragraph, list, or whatever, and you should be fine.
<form><divclass="control"><labelfor="full_name">Full name</label><inputid="full_name"type="text" /><pclass="description"tabindex="0">
Please tell us your name.
</p></div><divclass="control"><labelfor="biography">Biography</label><textareaid="biography"></textarea><pclass="description"tabindex="0">
Please tell us something about your history.
</p></div><fieldset><legend>Gender</legend><divclass="control"><inputid="gender_male"name="gender"type="radio" /><labelfor="gender_male">Male</label><pclass="description"tabindex="0">
You may be this if you like beer, gadgets, and Playboy Magazine.
</p></div><divclass="control"><inputid="gender_female"name="gender"type="radio" /><labelfor="gender_female">Female</label><pclass="description"tabindex="0">
You may be this if you like prosecco, make up, and Playgirl Magazine.
</p></div></fieldset><divclass="control"><labelfor="age_group">Age Group</label><selectid="age_group"><option>
0 to 9 Years
</option><option>
10 to 19 Years
</option><option>
20 to 29 Years
</option><option>
30 to 39 Years
</option><option>
40 to 49 Years
</option><option>
50 to 59 Years
</option><option>
60 to 69 Years
</option><option>
70 to 79 Years
</option><option>
80 to 89 Years
</option><option>
90 to 99 Years
</option></select><pclass="description"tabindex="0">
Tell us how old you are.
</p></div><divclass="control"><inputid="accept_agbs"type="checkbox" /><labelfor="accept_agbs">I accept the following terms and conditions:</label><ultabindex="0"><li>
I entered all data truthfully
</li><li>
I'm a good person
</li><li>
I won't vote for Donald Trump next time
</li></ul></div></form>
While this may be tempting, it is very bad style: only elements that provide some interaction (buttons, links, form controls, etc.) should be focusable. Otherwise keyboard only users may be confused, as they may think that (for example) a paragraph can be interacted with, although it cannot.
Also, it is unfortunate that this way the descriptive text of a form control is announced to screen readers after perceiving (and possibly interacting with) it.
Placing more content into labels
HTML allows to put various content into a <label>. On one hand, it is possible this way to put a form control right into its label, which even removes the need for the for attribute (if you haven't done this yet, go back and read General good form example).
<label>
Name
<input type="text" />
</label>
On the other hand (which is more interesting to us), it is also possible to put additional content into it.
<label>
Name
<input type="text" />
Please enter your full real name.
</label>
This solution looks very easy and as such is tempting: it does not only solve our requirement, but it also enlarges the clickable area of the label for mouse and touch screen users (clicking on the label places the focus into its control).
<form><labelclass="control"><divclass="label">
Name
</div><inputtype="text" /><divclass="description">
Please enter your full real name.
</div></label><labelclass="control"><divclass="label">
Biography
</div><textarea></textarea><divclass="description">
Please tell us something about your history.
</div></label><fieldset><legend>Gender</legend><labelclass="control"><inputname="gender"type="radio" /><divclass="label">
Male
</div><divclass="description">
You may be this if you like beer, gadgets, and Playboy Magazine.
</div></label><labelclass="control"><inputname="gender"type="radio" /><divclass="label">
Female
</div><divclass="description">
You may be this if you like prosecco, make up, and Playgirl Magazine.
</div></label></fieldset><labelclass="control"><divclass="label">
Age Group
</div><select><option>
0 to 9 Years
</option><option>
10 to 19 Years
</option><option>
20 to 29 Years
</option><option>
30 to 39 Years
</option><option>
40 to 49 Years
</option><option>
50 to 59 Years
</option><option>
60 to 69 Years
</option><option>
70 to 79 Years
</option><option>
80 to 89 Years
</option><option>
90 to 99 Years
</option></select><divclass="description">
Tell us how old you are.
</div></label><labelclass="control"><inputtype="checkbox" /><divclass="label">
I accept the following terms and conditions:
</div><ul><li>
I entered all data truthfully
</li><li>
I'm a good person
</li><li>
I won't vote for Donald Trump next time
</li></ul></label></form>
In Firefox this works like a charm (both JAWS and NVDA).
In the example, the radio buttons for gender are correctly announced, like "Gender grouping. Male. You may be this if you like beer…".
In Internet Explorer things are messed up when elements are packed into a <fieldset>/<legend> structure.
In the example, the radio buttons for gender are both announced like "Gender. Name. Please enter your full real name."
What a bummer. This is why we recommend to separate form controls and their <label> elements strictly and to use the for attribute to connect them.
And for associating additional content to a control ("Please enter…"), we advise to stick to better solutions (if you are really curious and want to learn more about this, skip ahead and read Placing non-interactive content between form controls), at least if you are using <fieldset>/<legend> structures.
More than one label per input
It is perfectly valid HTML to associate more than a single <label> element to an input. But Internet Explorer sadly connects only one of them, resulting in incomplete desktop screen reader announcements.
<form><divclass="control"><labelfor="full_name">Full name</label><inputid="full_name"type="text" /><labelclass="description"for="full_name">Please tell us your name.</label></div><divclass="control"><labelfor="biography">Biography</label><textareaid="biography"></textarea><labelclass="description"for="biography">Please tell us something about your history.</label></div><fieldset><legend>Gender</legend><divclass="control"><inputid="gender_male"name="gender"type="radio" /><labelfor="gender_male">Male</label><labelclass="description"for="gender_male">You may be this if you like beer, gadgets, and Playboy Magazine.</label></div><divclass="control"><inputid="gender_female"name="gender"type="radio" /><labelfor="gender_female">Female</label><labelclass="description"for="gender_female">You may be this if you like prosecco, make up, and Playgirl Magazine.</label></div></fieldset><divclass="control"><labelfor="age_group">Age Group</label><selectid="age_group"><option>
0 to 9 Years
</option><option>
10 to 19 Years
</option><option>
20 to 29 Years
</option><option>
30 to 39 Years
</option><option>
40 to 49 Years
</option><option>
50 to 59 Years
</option><option>
60 to 69 Years
</option><option>
70 to 79 Years
</option><option>
80 to 89 Years
</option><option>
90 to 99 Years
</option></select><labelclass="description"for="age_group">Tell us how old you are.</label></div><divclass="control"><inputid="accept_agbs"type="checkbox" /><labelfor="accept_agbs">I accept the following terms and conditions:</label><labelclass="description"for="accept_agbs"><ul><li>
I entered all data truthfully
</li><li>
I'm a good person
</li><li>
I won't vote for Donald Trump next time
</li></ul></label></div></form>