You Can Cheat When Fixing HTML Accessibility in Legacy Code

How and when to cheat on semantic HTML accessibility guidelines

Dan Fabulich
Code Red

--

Dan Fabulich is a Principal Engineer at Redfin. (We’re hiring!)

I’ve been spending the last month or so updating some legacy HTML to support screen readers. Screen-reader software can read a web page aloud, allowing visually impaired users to understand and interact with the page.

The problem is, sometimes the developers used <span> or <div> elements where they could have used <button> elements. Keyboard users can focus <button> elements with the Tab key, and click on them with the Enter key or Space bar; screen readers use the same rules.

By default, none of those keyboard shortcuts work for <span> or <div> elements. (In some cases, developers used <a> elements without an href attribute, which, as we’ll see, is no better than a <span> for accessibility.)

The “right thing” is to convert these elements into proper <button> elements, and, if needed, update the corresponding CSS rules to match the <button> instead of the old <span>/<div> element.

But the right thing turns out to be harder than you might think. Below are a few situations where the “right thing” can run into trouble; where possible, I’ve provided ways to cheat.

It’s like the right thing, but … different.

Buttons Can’t Display Inline

Check out this sample. It includes a simple <button> with all: unset anddisplay: inline.

Even though we forced the button to display: inline, you'll find that it's using display: inline-block, ignoring the style we set. That's why the “bar” button wraps to the second line, rather than appearing on the same line as “foo.”

This is the specified behavior in the HTML Rendering specification.

10.5.2. The button element

The <button> element is expected to render as an 'inline-block' box rendered as a button whose contents are the contents of the element.

This language is a bit cryptic, but what it means is that <button> elements will refuse to display: inline, even if you use !important.

Use the Dark Side: Make a Fake Accessible Button

The only way to cook up a button with display: inline is to do the “wrong” thing, using a non-button <span> element with an onclick handler.

To make those spans behave like buttons in a screen reader, you can add the role="button" attribute and the tabindex="0" attribute. You'll also want to add a keydown handler to handle the Enter and Space keys, like this:

span.addEventListener('keydown', function(e) {
if (e.keyCode === 32 /*Space*/ || e.keyCode === 13 /*Enter*/) {
e.target.click();
}
});

In my case, this is very close to what was already there in the legacy code, so adding the roleand tabindex attributes plus a keydown handler is sometimes quicker and easier than doing the “right thing.”

By using the tag name that was already in the code, I didn’t need to modify any CSS to fix selectors that were expecting a <span>.

Emperor Palpatine would be proud

Buttons Will Submit Forms by Default

If the <div> that we're converting to a <button> happens to appear in a <form>, you'll find that the <button> becomes a submit button for the form.

That’s probably not what you intended!

<button> is implicitly <button type="submit"> by default. To prevent this, you can use <button type="button"> instead.

type="button" is clearly defined in the HTML spec:

Do nothing.

That’s my kind of button! Some people say that nothing is impossible, but type="button"proves them all wrong.

(Or you can use a fake button, as above.)

Links Without Href Aren’t Accessible

Occasionally, UI designers call for buttons that have link-like styling. In the legacy code I’m working on, I see a lot of <a> elements with no href and an onclick handler; these are effectively buttons that look like links.

But code like this is secretly busted:

<a onclick="alert('hi')">hi</a> 

In Chrome, that HTML won’t even appear as a link; it won’t have the default underline styling, and it won’t have a cursor pointer when you hover over it. Developers sometimes work around that by adding styling:

a {
text-decoration: underline;
cursor: pointer;
}

… but that doesn’t fix the accessibility issues with these broken <a> elements. You can't use the Tab key to select these broken links, and you can't use the Enter key to use them.

<a> without href is no better than a <span>.

So, what can we do?

  1. Add a fake href to the link, e.g. href="javascript:void(0)"
  2. Convert the <a> into a <button>, but beware the issues raised above.
  3. Convert the <a> into a fake button, as above. They'll need tabindex="0" and a keydown handler. While you're at it, you might as well add role="button".

Keyboard Accessibility Requires a Visible Focus State

The default focus outline for links and buttons doesn’t always look great. Some developers “fix” this by removing the outline entirely, with outline: none styling on all buttons and links.

But when you’re tabbing around the screen, the focus outline is the only way to know which element is selected. Thus, outline: none breaks keyboard navigation, even if all of the elements are technically keyboard navigable.

(This is an example where we need to think about multiple forms of accessibility. Blind users may not benefit from visible focus states if their screen reader provides auditory cues to indicate which element is selected, but other users who require a keyboard will benefit.)

In my case, the original designers didn’t like the default focus outline, but they were careful about adding a nice hover state for all elements, using the :hover pseudo-selector. By adding the :focus pseudo-selector to those same rules, I could use our lovely hover state as the focus state.

That’s all for now! Now get out there and do your best.

P.S. Redfin is hiring.

--

--