Grouping form controls with fieldset and legend
Grouping form controls in a meaningful way can make them much easier to handle for everyone. While most controls can be grouped, some must be grouped. The fieldset/legend structure is available for exact that - it can even be nested. And if its visual limitations are a problem, ARIA can be of help.
<fieldset>
/<legend>
structures are used for grouping form controls that are related in some way. While in complex forms they can be used to group all kinds of form controls, groups of radio buttons and checkboxes should always be grouped by them.
Grouping any kinds of controls
Grouping thematically
The more complex forms are, the more it makes sense to group thematically related form controls of any kind.
Grouped form controls with too long legend
<form>
<fieldset>
<legend>Your address</legend>
<div class="control">
<label for="first_name">First name</label><input id="first_name" type="text" />
</div>
<div class="control">
<label for="last_name">Last name</label><input id="last_name" type="text" />
</div>
<div class="control">
<label for="street">Street</label><input id="street" type="text" />
</div>
<div class="control">
<label for="city">City</label><input id="city" type="text" />
</div>
</fieldset>
<fieldset>
<legend>Additional details</legend>
<div class="control">
<label for="biography">Biography</label><textarea id="biography"></textarea>
</div>
<div class="control">
<label for="image">Image</label><input id="image" type="file" />
</div>
<div class="control">
<label for="age_group">Age Group</label><select 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>
</div>
</fieldset>
<fieldset>
<legend>Interests</legend>
<div class="control">
<label for="hobbies">Hobbies</label><select id="hobbies" multiple="">
<option>
Playing soccer
</option>
<option>
Dancing
</option>
<option>
Gardening
</option>
<option>
Watching movies
</option>
</select>
</div>
<div class="control">
<label for="favourite_car">Favourite car?</label><select id="favourite_car"><optgroup label="Swedish Cars">
<option>
Volvo
</option>
<option>
Saab
</option>
</optgroup><optgroup label="German Cars">
<option>
Mercedes
</option>
<option>
Audi
</option>
</optgroup></select>
</div>
<div class="control">
<label for="public_transports">Favourite public transport?</label><select id="public_transports" multiple=""><optgroup label="Ground">
<option>
Train
</option>
<option>
Bus
</option>
</optgroup><optgroup label="Water">
<option>
Ship
</option>
<option>
Submarine
</option>
</optgroup><optgroup label="Air">
<option>
Plane
</option>
<option>
Cable Car
</option>
</optgroup></select>
</div>
</fieldset>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 200px;
vertical-align: top;
}
input + label {
width: auto;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
By the way, it is not mandatory to group all your form controls. In the example above, you could easily drop the grouping of interests (or any other). In general: use groupings of thematically related controls whenever it feels meaningful to you and your users.
Grouping to differentiate
Another use case is differentiating groups with similar input fields, for example to tell apart shipping and billing address.
Grouped and differentiated form controls
<form>
<fieldset>
<legend>Billing address</legend>
<div class="control">
<label for="billing_first_name">First name</label><input id="billing_first_name" type="text" />
</div>
<div class="control">
<label for="billing_last_name">Last name</label><input id="billing_last_name" type="text" />
</div>
<div class="control">
<label for="billing_street">Street</label><input id="billing_street" type="text" />
</div>
<div class="control">
<label for="billing_city">City</label><input id="billing_city" type="text" />
</div>
</fieldset>
<fieldset>
<legend>Shipping address</legend>
<div class="control">
<label for="shipping_first_name">First name</label><input id="shipping_first_name" type="text" />
</div>
<div class="control">
<label for="shipping_last_name">Last name</label><input id="shipping_last_name" type="text" />
</div>
<div class="control">
<label for="shipping_street">Street</label><input id="shipping_street" type="text" />
</div>
<div class="control">
<label for="shipping_city">City</label><input id="shipping_city" type="text" />
</div>
</fieldset>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
Grouping certain kind of controls
Radio buttons
Groups of radio buttons are used as possible answers to a certain question, and as such they are always related to each other. Hence they are always grouped, while the <fieldset>
's <legend>
contains the question and each control's <label>
contains an answer.
Grouped radio buttons
<form>
<fieldset>
<legend>Gender</legend>
<div class="control">
<input id="gender_male" name="gender" type="radio" /><label for="gender_male">Male</label>
</div>
<div class="control">
<input id="gender_female" name="gender" type="radio" /><label for="gender_female">Female</label>
</div>
</fieldset>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
input + label {
width: auto;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
This is also reflected by the fact that each radio button's name
attribute is identical.
Checkboxes
Groups of checkboxes are very similar to radio buttons, and hence are grouped most of the time:
Grouped checkboxes
<form>
<fieldset>
<legend>Hobbies</legend>
<div class="control">
<input id="hobbies_hiking" type="checkbox" /><label for="hobbies_hiking">Hiking</label>
</div>
<div class="control">
<input id="hobbies_dancing" type="checkbox" /><label for="hobbies_dancing">Dancing</label>
</div>
<div class="control">
<input id="hobbies_gardening" type="checkbox" /><label for="hobbies_gardening">Gardening</label>
</div>
</fieldset>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
In contrast to radio buttons though, checkboxes do not imperatively relate to each other. So there may be situations where a number of non-related checkboxes can stand on their own, without being grouped:
Non related checkboxes
<form>
<div class="control">
<input id="pizza" type="checkbox" /><label for="pizza">I like pizza.</label>
</div>
<div class="control">
<input id="weather" type="checkbox" /><label for="weather">The weather is sunny.</label>
</div>
<div class="control">
<input id="info" type="checkbox" /><label for="info">I would like to receive more information about this.</label>
</div>
</form>
.control {
margin: 6px 0;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
This is also reflected by the fact that each checkbox's name
attribute is different.
Notice: single checkboxes, like "Accept terms and conditions", do not need a <fieldset>
/<legend>
. Still, it can make perfect sense to put them into their own <fieldset>
/<legend>
, maybe together with some related link(s) or text (see Placing non-interactive content between form controls).
Nesting
Nesting <fieldset>
/<legend>
structures is possible, especially when groups of radio buttons or checkboxes play a role.
Nested fieldset/legend structures
<form>
<fieldset>
<legend>Personal Details</legend>
<div class="control">
<label for="name">Full name</label><input id="name" type="text" />
</div>
<div class="control">
<label for="biography">Biography</label><textarea id="biography"></textarea>
</div>
<fieldset>
<legend>Gender</legend>
<div class="control">
<input id="gender_male" name="gender" type="radio" /><label for="gender_male">Male</label>
</div>
<div class="control">
<input id="gender_female" name="gender" type="radio" /><label for="gender_female">Female</label>
</div>
</fieldset>
</fieldset>
<div class="control">
<input id="accept_agbs" type="checkbox" /><label for="accept_agbs">I accept the terms and conditions</label>
</div>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
input + label {
width: auto;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
But you should not overdo that: while screen readers uniformly announce when a grouping is entered, some do not announce when it is exited. As such, too much nesting can quickly become confusing.
Keeping legends short
Regarding <fieldset>
/<legend>
structures, some screen readers are much wordier than others.
On one side, JAWS attaches the <legend>
's text to the label of each contained form control. For a <legend>
with the text "Personal Details" and two fields "First Name" and "Last Name", it literally announces:
Personal Details, First Name.
Personal Details, Last Name.
On the other side, NVDA only announces the <legend>
's text when entering the <fieldset>
. For the above example, it literally announces:
Personal Details, Grouping. First Name.
Last Name.
Grouped form controls with too long legend
<form>
<fieldset>
<legend>Please fill out the following inputs truthfully and completely. We will get back to you as soon as possible. Thank you for your effort.</legend>
<div class="control">
<label for="name">Full name</label><input id="name" type="text" />
</div>
<div class="control">
<label for="biography">Biography</label><textarea id="biography"></textarea>
</div>
<div class="control">
<label for="age_group">Age Group</label><select 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>
</div>
</fieldset>
</form>
.control, fieldset {
margin: 6px 0;
}
label {
display: inline-block;
width: 120px;
vertical-align: top;
}
input + label {
width: auto;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
Conclusion: never put long texts into the <legend>
element. If you really need to put long text into a form, there are other approaches, see Placing non-interactive content between form controls.
Legend must be a direct child of fieldset
HTML requires the <legend>
element to be the first child of <fieldset>
.
This is correct:
<fieldset>
<legend>Gender</legend>
</fieldset>
This is problematic:
<fieldset>
<div>
<legend>Gender</legend>
</div>
</fieldset>
This is also problematic:
<fieldset>
<div>...</div>
<legend>Gender</legend>
</fieldset>
As a result, some screen readers seem to have difficulties associating the <legend>
correctly to its <fieldset>
if it is not its first child.
Overcoming visual limitations using ARIA
Admittedly, browsers have a quite peculiar opinion on how to visually style <fieldset>
/<legend>
structures, and it is quite hard to override their preferences.
So if for visual (or any other serious) reason you cannot use standard <fieldset>
/<legend>
, you may take ARIA to the rescue:
- Use
role="group"
to give a <div>
container the semantics of a <fieldset>
.
- Associate any other text to the grouping container using
aria-describedby
.
Faked fieldset/legend using ARIA
<form>
<div aria-describedby="gender_legend" class="fieldset" role="group">
<div class="legend" id="gender_legend">
Gender
</div>
<div class="control">
<input id="gender_male" name="gender" type="radio" value="male" /><label for="gender_male">Male</label>
</div>
<div class="control">
<input id="gender_female" name="gender" type="radio" value="female" /><label for="gender_female">Female</label>
</div>
</div>
</form>
.control, .fieldset {
margin: 6px 0;
}
label, .label {
display: inline-block;
width: 120px;
vertical-align: top;
}
input + .label {
width: auto;
}
.fieldset {
position: relative;
border: 2px groove threedface;
padding: 10px;
margin-bottom: 20px;
}
legend, .legend {
background-color: #fff;
position: absolute;
left: 10px;
top: -12px;
padding: 2px;
}
Category |
Result |
Date |
Keyboard only |
✔ (pass) pass
|
- |
2018-5-14 |
NVDA 2018.1 + FF Quantum 59.0.2 |
✔ (pass) pass
|
- |
2018-5-8 |
JAWS 2018.3 + IE 11 |
✔ (pass) pass
|
- |
2018-5-13 |
JAWS 2018.3 + FF ESR 52.7.3 |
✔ (pass) pass
|
- |
2018-5-7 |
As always, we highly recommend to use traditional solutions over ARIA, so if you haven't done this yet, go back and read ARIA - when HTML simply is not enough.