Question About Assessible, Complex Data Entry Forms

This week’s question comes from Luís Osório.

If I have a form, and I want to collect tabular data, I would like to make a table with th for row and column headers and then, in each td I would like to have an input (type=text). The thing is … an input must have a label and each td should reference the th. How should this be done using a table?

Sherpa Jared Smith answers:

Your situation has two distinct accessibility requirements: table cells should be associated to their respective row and/or column headers, and inputs need descriptive labels. We’ll address these individually.

Data Table Accessibility

First, you might ask whether the table really is a data table at all, or whether it is primarily being used to lay out and position the data entry fields. It is most likely that users will navigate from form field to form field, opposed to navigating the table structure. So, adding the table accessibility markup is not likely to have a notable impact.

But it’s easy to add. Simply identify each column header cell with <th scope="col"> (as opposed to td for table data cells):

<th scope="col">Name</th>

If you have row headers, use <th scope="row">. Just make sure that you don’t have empty th elements, and that empty or data cells always use td. That’s it for the table.

Form Accessibility

For the form, label the inputs by associating a label element to an input based on the input’s id attribute as follows:

<label for="fname">First Name:</label> <input id="fname" type="text">

This works great for most inputs that have a visual label adjacent to them. But for your data entry fields, it’s most likely that one text item (probably your column header) will be used to visually label multiple inputs. Because the id attribute must be unique per page (e.g., you can’t have two elements that each have id="fname" on one page), this creates a one–to–one relationship between form label and input.

Labelling Multiple Fields

While we can’t label multiple inputs using one label in a column header, there are several approaches to ensure accessibility. One approach would be to use an off-screen label element for each input. Simply add the associated label adjacent to each control in your HTML, but hide the label element off-screen using CSS. Sighted users will see the column header, and screen reader users will hear the associated off-screen label text. This works well, but requires a lot of additional markup.

Alternatively, you could use a descriptive title attribute value on each input. Screen readers generally ignore the title attribute, but will read it for inputs that do not have an associated label. This will also generate a mouse tooltip on the input, which may or may not be useful.

Using ARIA

As a final, and perhaps optimal, approach, you could use the aria-labelledby attribute to overcome the one–to–one label to input limitation. The aria-labelledby attribute goes on the input itself and references the element(s) on the page (based on id attribute values) that contains the input’s label text. The screen reader will read the text within the associated element as the label text when it encounters the input:

<input aria-labelledby="fname">

For your data entry form, each table header would have a unique id value. Then each input in that header’s row or column would reference that header as an ARIA label. You can even have multiple aria-labelledby values for each input — they will be read in the order listed. This is particularly helpful with data entry.

    <th scope="col">Name</th>
    <th id="agelabel" scope="col">Age</th>
    <th id="phonelabel" scope="col">Phone</th>
    <th id="jared" scope="col">Jared</th>
    <td><input type="text" size="3" name="age1" aria-labelledby="jared agelabel"></td>
    <td><input type="text" size="12" name="phone1" aria-labelledby="jared phonelabel"></td>
    <th id="luis" scope="col">Luís</th>
    <td><input type="text" size="3" name="age2" aria-labelledby="luis agelabel"></td>
    <td><input type="text" size="12" name="phone2" aria-labelledby="luis phonelabel"></td>
    <th id="aaron" scope="col">Aaron</th>
    <td><input type="text" size="3" name="age3" aria-labelledby="aaron agelabel"></td>
    <td><input type="text" size="12" name="phone3" aria-labelledby="aaron phonelabel"></td>

Each input in this case would have aria-labelledby values that reference both the column and row header cells based on their id attribute values:

<input aria-labelledby="jared agelabel" type="text">

This final approach has great support in all modern browsers and screen readers, and it provides great flexibility when dealing with complex data entry forms.