Showing Good Form

Earlier this year, I forget exactly when (it’s been a good year), I was building a client site that needed widgets which look like this (designed, incidentally, by my erstwhile writing partner, Cameron Adams):

A grid of form elements laid out like a table, in three rows of four columns. Down the left side are row headers:

Building this was a challenge not just in CSS, but in choosing the proper markup – how should such a widget be constructed?

Mmm … markup

It seemed to me there were two key issues to deal with:

  • The function of the interface is to input information, so semantically this is a form, therefore we have to find a way of building it using form elements: fieldset, legend, label and input
  • We can’t use a table for layout, even though that would clearly be the easiest solution!

Abusing tables for layout is never good – physical layout is not what table semantics mean. But even if this data can be described as a table, we shouldn’t mix forms markup with non-forms markup, because of the behavioral impact this can have on a screen reader:

To take a prominent example, the screen reader JAWS has a mode specifically for interacting with forms (cunningly known as “forms mode”). When running in this mode its output only includes relevant elements – legends, labels and form controls themselves. Any other kind of markup – like text in a previous table cell, a paragraph or list in between – is simply ignored. The user in this situation would have to switch continually in and out of forms mode to hear all the content. (For more about this issue and some test examples, there’s a thread at accessify forum which wanders in that direction.)

One further issue for screen reader users is implied by the design: the input fields are associated together in rows and columns, and a sighted user can visually scan across and down to make those associations; but a blind user can’t do that. For such a user the row and column header data will need to be there at every axis; in other words, the layout should be more like this:

The same grid of form elements as before, but instead of the column headers only appearing across the top, they're repeated in between every row of form elements.

And constructed with appropriate semantic markup to convey those relationships. By this point the selection of elements seems pretty clear: each row is a fieldset, the row header is a legend, and each column header is a label, associated with an input.

Here’s what that form looks like with no CSS:

The same set of form elements as before, but this time unstyled: the elements are laid out in three groups (one for each row, e.g.

And here’s some markup for the first row (with most of the attributes removed just to keep this example succinct):

<fieldset>
	<legend>
		<span>Match points</span>
	</legend>
	<label>
		<span>Win</span>
		<input value="3" />
	</label>
	<label>
		<span>Draw</span>
		<input value="1" />
	</label>
	<label>
		<span>Lose</span>
		<input value="0" />
	</label>
	<label>
		<span>Played</span>
		<input value="0" />
	</label>
</fieldset>

The span inside each legend is because legend elements are highly resistant to styling! Indeed they’re one of the most stubborn elements in the browsers’ vocabulary. Oh man … how I wrestled with the buggers … until this obvious alternative occurred to me! So the legend element itself is just a container, while all the styling is on the inner span.

Oh yeah, there was some CSS too

I’m not gonna dwell too much on the CSS it took to make this work – this is a short article, and it’s all there in the demo [demo page, style sheet]

But I do want to touch on the most interesting bit – where we get from a layout with headers on every row, to one where only the top row has headers – or at least, so it appears to graphical browsers. For screen readers, as we noted, we need those headers on every row, so we should employ some cunning CSS to partly negate their visual presence, without removing them from the output.

The core styling for each label span is like this:

label span
{
	display:block;
	padding:5px;
	line-height:1em;
	background:#423221;
	color:#fff;
	font-weight:bold;
}

But in the rows below the header they have these additional rules:

fieldset.body label span
{
	padding:0 5px;
	line-height:0;
	position:relative;
	top:-10000em;
}

The rendered width of the element is preserved, ensuring that the surrounding label is still the same width as the one in the header row above, and hence a unified column width is preserved all the way down. But the element effectively has no height, and so it’s effectively invisible. The styling is done this way, rather than just setting the height to zero and using overflow:hidden, because to do that would expose an unrelated quirk with another popular screen reader! (It would hide the output from Window Eyes, as shown in this test example at access matters.)

The finished widget

It’s an intricate beast allright! But after all that we do indeed get the widget we want:

It’s not perfect, most notably because the legends have to have a fixed width; this can be in em to allow for text scaling, but it still doesn’t allow the content to break into multiple lines. It also doesn’t look quite right in Safari; and some CSS hacking was needed to make it look right in IE6 and IE7.

Still it worked well enough for the purpose, and satisfied the client completely. And most of all it re-assured me in my faith – that there’s never any need to abuse tables for layout. (Unless of course you think this content is a table anyway, but that’s another story!)

About the author

James Edwards (aka brothercake) is a freelance web developer based in the United Kingdom, specialising in advanced JavaScript programming and accessible website development. He is an outspoken advocate of standards-based development, an active member of WaSP (The Web Standards Project), and creator of the Ultimate Drop Down Menu system - the first commercial DHTML menu to be WCAG compliant. James was also co-author of The JavaScript Anthology, published by SitePoint in 2006.

More articles by James

Comments