Building accessible web forms with ASP.NET

First of all, let me welcome you to my brand new blog. It will serve as a personal reference and a way to (hopefully) become a better software developer by researching and writing about technology. And don't forget if you liked the article, subscribe to the RSS. It's free and saves you time!

Now let's get our hands dirty...

When designing a web form with ASP.NET you usually fire up the web forms designer that comes with Visual Studio and start dragging and dropping server controls on the page. This is the normal way of designing a web form, the way Microsoft promotes. They have provided us with a Label server control which we all use to put a text near a certain TextBox, DropDownList or something similar.

The markup for a trivial form might look like this:

<form id="form1" runat="server">
	<fieldset>
		<legend>Default ASP.Net form</legend>
		<asp:label runat="server" id="userNameLabel">username:</asp:label>
		<asp:textbox runat="server" id="userName"></asp:textbox>
		<asp:label runat="server" id="passwordLabel">password:</asp:label>
		<asp:textbox runat="server" id="password"></asp:textbox>
		<hr />
		<asp:label runat="server" id="streetLabel">street:</asp:label>
		<asp:textbox runat="server" id="street"></asp:textbox>
		<asp:label runat="server" id="numberLabel">number:</asp:label>
		<asp:textbox runat="server" id="number"></asp:textbox>
		<asp:label runat="server" id="zipCodeLabel">zipCode:</asp:label>
		<asp:textbox runat="server" id="zipCode"></asp:textbox>
		<asp:label runat="server" id="cityLabel">city:</asp:label>
		<asp:textbox runat="server" id="city"></asp:textbox>
		<hr />
		<asp:button runat="server" id="submit" text="Submit" cssclass="button" />
	</fieldset>
</form>

Let's not get carried away with the look and feel of this form, it could be better, but that's not what what we care about in this article. Let's have a look at what happens under the surface, if we look at the source of this page we see the following piece of generated HTML:

<form name="form1" method="post" action="Default.aspx" id="form1">
	<div>
		<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/…" />
	</div>
	<fieldset>
		<legend>Default ASP.Net form</legend>
		<span id="userNameLabel">username:</span>
		<input name="userName" type="text" id="userName" />
		<span id="passwordLabel">password:</span>
		<input name="password" type="text" id="password" />
		<hr />
		<span id="streetLabel">street:</span>
		<input name="street" type="text" id="street" />
		<span id="numberLabel">number:</span>
		<input name="number" type="text" id="number" />
		<span id="zipCodeLabel">zipCode:</span>
		<input name="zipCode" type="text" id="zipCode" />
		<span id="cityLabel">city:</span>
		<input name="city" type="text" id="city" />
		<hr />
		<input type="submit" name="submit" value="Submit" id="submit" class="button" />
	</fieldset>
	<div>
		<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/…/U0=" />
	</div>
</form>

The problem with the Label server control is that it generates a <span>-tag. The browser has no clue that a given label applies on a given input field. And the <span> doesn't describe its content, from a semantic point of view it's not right. Why is this necessary? Don't you see which label applies on which input field? That's right, for us it's most of the time easy to link a certain label to an input field. But visually impaired users have a harder time trying to figure out what they have to fill into the input field. If the input field and the label aren't close to each other, it's impossible for a screen reader to make the link between the label and the input field.

Instead of using the Label server control, we could use the <label>-tag. The html <label>-tag describes its content, in this case it describes that the text in front of the textbox, dropdownlist, … is actually the label for that input field. This is a much better practice and not only from a semantic point of view. Another benefit of using the html tag is that we can style it very easily via CSS. In order to couple the <label> to a certain input field we use the for attribute. The value of the for attribute should be the ID of the input field which the label describes.

If we change our form and use the newly learned <label>-tag, we get the following code:

<form name="form1" method="post" action="Default.aspx" id="form1">
	<div>
		<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/…" />
	</div>
	<fieldset>
		<legend>Default ASP.Net form</legend>
		<span id="userNameLabel">username:</span>
		<input name="userName" type="text" id="userName" />
		<span id="passwordLabel">password:</span>
		<input name="password" type="text" id="password" />
		<hr />
		<span id="streetLabel">street:</span>
		<input name="street" type="text" id="street" />
		<span id="numberLabel">number:</span>
		<input name="number" type="text" id="number" />
		<span id="zipCodeLabel">zipCode:</span>
		<input name="zipCode" type="text" id="zipCode" />
		<span id="cityLabel">city:</span>
		<input name="city" type="text" id="city" />
		<hr />
		<input type="submit" name="submit" value="Submit" id="submit" class="button" />
	</fieldset>
	<div>
		<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/…/U0=" />
	</div>
</form>

Because the different <label>-tags are html elements and not server controls, ASP.NET won't mess with the markup and just leave it as it is. The HTML code that is generated is much cleaner, much more accessible specially for visually impaired visitors and on top we save on viewstate (if viewstate is not turned off, which you should whenever possible)!

A careful reader is asking himself right now: but what if I'm using master pages or a user control? When using master pages or user controls, ASP.NET changes the ID of the server controls in order to make them unique (certainly the case with user controls). To solve this we use some inline coding tags to get the ClientID of the server control into the for attribute of the corresponding <label>-tag. Did I lost you? Let's alter our example to make a bit more clear:

<form id="form1" runat="server">
	<fieldset>
		<legend>Accessible, without master</legend>
		<label for="<%= userName.ClientID %>">username:</label>
		<asp:TextBox runat="server" ID="userName"></asp:TextBox>
		<label for="<%= password.ClientID %>">password:</label>
		<asp:TextBox runat="server" ID="password"></asp:TextBox>
		<hr />
		<label for="<%= street.ClientID %>">street:</label>
		<asp:TextBox runat="server" ID="street"></asp:TextBox>
		<label for="<%= number.ClientID %>">number:</label>
		<asp:TextBox runat="server" ID="number"></asp:TextBox>
		<label for="<%= zipCode.ClientID %>">zipcode:</label>
		<asp:TextBox runat="server" ID="zipCode"></asp:TextBox>
		<label for="<%= city.ClientID %>">city:</label>
		<asp:TextBox runat="server" ID="city"></asp:TextBox>
		<hr />
		<asp:Button runat="server" ID="submit" Text="Submit" CssClass="button" />
	</fieldset>
</form>

As you can see we use inline coding tags to fetch the clientID of the server control and fill it into the for attribute of the <label>-tag. If we look at the output of this page (it is wrapped in a master page) then we get the following html code (the input field ids have been truncated for your reading pleasure):

<fieldset>
	<legend>Accessible</legend>
	<label for="ctl00_…_userName">username:</label>
	<input name="ctl00$…$userName" type="text" id="ctl00_…_userName" />
	<label for="ctl00_…_password">password:</label>
	<input name="ctl00$…$password" type="text" id="ctl00_…_password" />
	<hr />
	<label for="ctl00_…_street">street:</label>
	<input name="ctl00$…$street" type="text" id="ctl00_…_street" />
	<label for="ctl00_…_number">number:</label>
	<input name="ctl00$…$number" type="text" id="ctl00_…_number" />
	<label for="ctl00_…_zipCode">zipcode:</label>
	<input name="ctl00$…$zipCode" type="text" id="ctl00_…_zipCode" />
	<label for="ctl00_…_city">city:</label>
	<input name="ctl00$…$city" type="text" id="ctl00_…_city" />
	<hr />
	<input type="submit" name="…" value="Submit" id="…" class="button" />
</fieldset>

The for-attribute of each label matches the id of the input fields. And even if the page gets moved to a user control, we don't have to change anything. The ClientID is automatically filled out for us. Another nice side effect of using this technique: When clicking on a <label>-tag, the corresponding input field gets focus. This kind of behavior is implemented in every modern browser (and Internet Explorer 6).
I use this technique to create all my forms.

Useful reading:

Update: Chris pointed out in the comments that there is also an AssociatedControlID property on a <asp:label>. If you set this property to the ID of your server control, then a <label> is outputted which will automatically insert the correct for-attribute. I did a Google search on the property and found out that the famous Phil Haack has written an excellent blog post about the subject a while ago, go check it out.