Skip navigation

I am <legend>: the form element with special powers

Posted Jan 11, 2020

If you know anything about web accessibility, then you probably already know that all <input> elements need to have a <label> (or failing that, an aria-supplied label).

What you may not know is what to do when you have something like this:

What do you have for sale? Radio button one says 'spam', radio button two says 'more spam'.

The radio buttons have... two labels. How do you have two labels?

2 labels 1 input

Now you might be forgiven for thinking that the way to solve this problem is as simple as it sounds:

<label>
    What do you have for sale?
    <label>
        <input type="radio" name="sale" value="spam">
        Spam
    </label>
    <label>
        <input type="radio" name="sale" value="morespam">
        More Spam
    </label>
</label>

Unfortunately this doesn't work. According to the spec, a <label> may contain:

Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.

You also can't do this:

<label for="input1 input2">What do you have for sale?</label>

Unfortunately, the for attribute only takes a single ID.

Great. So now what?

<fieldset> + <legend> to the rescue

Enter the dynamic duo: <fieldset> and <legend>.

You might already be familiar with the <fieldset> element as a way to group form controls and labels—that's the way it's described on the MDN page—but unfortunately that description is rather disappointingly vague and doesn't really explain what <fieldset> is meant for (more on that later). After all, a <section> groups things together as well—so what's the big deal aside from the special default styling?

Well, when paired with its sidekick, the <legend> element, it helps screen reader users by providing that second label that we need. Consider the following code:

<fieldset>
    <legend>What do you have for sale?</legend>
    <label>
        <input type="radio" name="sale" value="yes">
        Spam
    </label>
    <label>
        <input type="radio" name="sale" value="morespam">
        More Spam
    </label>
</fieldset>

When a screen reader encounters this code, it will announce the legend to the user along with the label.

Note 1: As per the spec, the <legend> element must be the first child of the <fieldset>.

Note 2: screen readers handle this information in slightly different ways though. JAWS will announce the legend for every input in the fieldset, whereas NVDA only announces it upon entering the fieldset. This latter point is something to keep in mind as you design your form, since NVDA users may not realize once they've exited the fieldset.

But wait, there's more!

I mentioned earlier that fieldset elements are meant for something more than just grouping related form controls. Well, strictly speaking that's true, but we should be more specific about what we mean by "related". After all, you could argue that most form inputs are related—that's why they're part of the same form. So, more accurately, we should say that fieldsets are meant for grouping related form controls that would be unclear or confusing if not grouped.

So what do I mean by that? Well consider a checkout form that asks for both a billing address and a shipping address:

Two sets of address inputs, one for billing address, the other for shipping address.

As a sighted user, I can easily distinguish which "street" input goes with which address, but for a screen reader user this could get confusing in a hurry. So, once again, here comes our superhero element friends to help us out:

<fieldset>
    <legend>Billing Address</legend>
    <label>
        Street Address
        <input type="text" name="streetAddress">
    </label>
    <label>
        City
        <input type="text" name="city">
    </label>
    <!-- more fields here -->
</fieldset><fieldset>
    <legend>Shipping Address</legend>
    <label>
        Street Address
        <input type="text" name="streetAddress">
    </label>
    <label>
        City
        <input type="text" name="city">
    </label>
    <!-- more fields here -->
</fieldset>

As with before, this may not be perfect for NVDA users due to the way it chooses to handle the legend, but it's a heck of a lot better than nothing.

What if I don't want to use <fieldset> and <legend>?

Actually there is a reason you may want to set aside the first rule of ARIA, and not use these elements. Remember how we mentioned earlier that <fieldset> and <legend> have default styling? Yeah... about that... it turns out they have very special default styling. This can make it rather difficult to work with, as you usually have to resort to floating and some funky margins to get the result you want.

So if we want to avoid that nonsense then we can achieve a similar result by doing this:

<div role="group" aria-labelledby="groupTitle">
    <h2 id="groupTitle">Billing Address</h2>
    <label>
        Street Address
        <input type="text" name="streetAddress">
    </label>
    <label>
        City
        <input type="text" name="city">
    </label>
    <!-- more fields here -->
</div>

What if I don't like groups, either?

You know, because you like making life difficult. [sigh]

If you're unable to wrap your fields in a grouping like this, there are some other ways around the problem. Just note that the drawback to these approaches is that things are going to get a little verbose for your screen reader users, since the full text will be announced for every input (as opposed to the <fieldset> or group options, where the verbosity is at the discretion of the screen reader software).

For this route, your options are...

Use hidden text

<label>
    Street Address <span class="visually-hidden">(billing address)</span>
    <input type="text" name="streetAddress">
</label>
<label>
    City <span class="visually-hidden">(billing address)</span>
    <input type="text" name="city">
</label>

Some aria-labelledby magic on the inputs themselves

Here we can take advantage of the fact that, unlike the for attribute, aria-labelledby can take a string-delimited list of IDs, instead of just a single one.

<h2 id="group-title">Billing Address</h2>
<label id="input1-label">City</label>
<input type="text name="city" aria-labelledby="group-title input1-label">

A good, ol'-fashioned aria-label

Not sure that aria-label qualifies as old-fashioned, but it makes for a good heading. Just be sure that your aria-label includes the same text as the visible label, ideally at the start. This is to ensure that people who use voice control software can focus the input by name.

<label for="city">City</label>
<input id="city" name="city" type="text" aria-label="City (Billing Address)">

Wrapping Up

So there you have it. A whole mess of options for ensuring that your form fields are properly identified for everyone.

Share

Contact

  • Contact me on Twitter
  • Contact me on LinkedIn
  • Contact me by Email