You Can Cheat When Fixing HTML Accessibility in Legacy Code
How and when to cheat on semantic HTML accessibility guidelines
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 role
and 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>
.
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?
- Add a fake
href
to the link, e.g.href="javascript:void(0)"
- Convert the
<a>
into a<button>
, but beware the issues raised above. - Convert the
<a>
into a fake button, as above. They'll needtabindex="0"
and akeydown
handler. While you're at it, you might as well addrole="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.