We often see links which have been styled to look like buttons. There is some debate among the accessibility community as to whether it is worthwhile adding a role="button"
to these elements so the semantics match the visuals.
I'm not going to get into that, but what I'm going to tackle is the approach which is taken when that role
is added.
Normally when we add a role="button"
to a link we add some simple js handling for the spacebar to allow users to activate the control in the same way they might activate a native button. Without this a user hitting the spacebar on a link which looks like a button would end up scrolling the page.
So we end up with something like this:
[].slice.call(document.querySelectorAll('a[role="button"]')).forEach(function (el) {
el.addEventListener('keypress', function (e) {
if (e.keyCode === 32) {
e.preventDefault();
el.click();
}
})
});
But that misses some of the nuances of the native control - something which is often missed when trying to replicate a native element. Native HTML buttons also have the feature where a user can cancel a spacebar trigger (just like you might cancel a mouse-click by pulling your mouse away before releasing it), by hitting the tab key whilst the spacebar is still pressed.
What we actually need to do is listen for two events.
First we need to listen for the keypress
event on the spacebar as above, as this is what allows us to cancel that native scroll which would happen otherwise.
Then we need to listen for the keyup
event on the spacebar. This means that if a user decides to cancel a “click” on our link-styled-as-a-button by using the tab key, we don't trigger it by mistake.
This ends up looking like this:
[].slice.call(document.querySelectorAll('a[role="button"]')).forEach(function (el) {
el.addEventListener('keypress', function (e) {
if (e.keyCode === 32) {
e.preventDefault();
}
})
el.addEventListener('keyup', function (e) {
if (e.keyCode === 32) {
e.preventDefault();
el.click();
}
})
});
Results
See below for the actual tests and detailed breakdown.
So we expected the first set of tests to be equal across the board and that is what we saw. Both handlers displayed the same characteristics as the native button. This proves we have not broken anything with the new handler.
However what we were really testing was if the native tab key interrupt was carried through with the handlers.
Benefits for keyboard-only users and some screen-reader users
It's pretty clear that the single event handler doesn't match the native button behaviour for keyboard-only users, whilst the double handler does a much better job of this.
It's also intersting to see that JAWS and NVDA don't allow for this interrupt but Voiceover does.
In conclusion it seems that a double handler like this provides a better solution for providing the correct behaviour for links with a button role.
The test methods and detailed breakdown
This is to test the standard practice of adding a single event listener (for the spacebar) against a proposed new double handler which prevents the standard scroll but also allows for the tab-interrupt.
Test 1 - standard spacebar activation
Move to the control with the Tab key and then activate the control with the spacebar.
This is a bit of a control test to make sure that the double handler works in the same way as the single handler and they both match the native expected outcome.
As expected both the single and double handlers perform the same with the standard test of triggering the control.
Browser / screenreader | Native button behaviour | Does single handler match native? | Does double handler match native? |
---|---|---|---|
Win Firefox | increments counter | yes | yes |
Win Chrome | increments counter | yes | yes |
Win Edge | increments counter | yes | yes |
MacOS Firefox | increments counter | yes | yes |
MacOS Chrome | increments counter | yes | yes |
MacOS Edge | increments counter | yes | yes |
MacOS Safari | increments counter | yes | yes |
NVDA Win Firefox | increments counter | yes | yes |
NVDA Win Chrome | increments counter | yes | yes |
JAWS Win Chrome | increments counter | yes | yes |
JAWS Win Edge | increments counter | yes | yes |
Voiceover MacOS Safari | increments counter | yes | yes |
Test 2 - activation interruption
The second test is to move to the link again but after pressing the spacebar, instead of releasing it, hold it down whilst pressing the Tab key.
This is the main test. We know native buttons have the option of cancelling a spacebar activation by using the tab key. This test checks how our different handlers cope with this and how different browsers and screen-readers handle it.
Browser / screenreader | Native button behaviour | Does single handler match native? | Does double handler match native? |
---|---|---|---|
Win Firefox | stops button triggering | no | yes |
Win Chrome | stops button triggering | no | yes |
Win Edge | stops button triggering | no | yes |
MacOS Firefox | stops button triggering | no | yes |
MacOS Chrome | stops button triggering | no | yes |
MacOS Edge | stops button triggering | no | yes |
MacOS Safari | stops button triggering | no | yes |
NVDA Win Firefox | increments counter | yes | yes |
NVDA Win Chrome | increments counter | yes | yes |
JAWS Win Chrome | increments counter | yes | yes |
JAWS Win Edge | increments counter | yes | yes |
Voiceover MacOS Safari | stops button triggering | no | yes |
The tests
Successful activations will increment the counter on the control.
The tab stop link between examples are just to separate the tests.
Control
Test 1 expected: button is activated.
Test 2 expected: tab key prevents spacebar from actioning the button.
Tab stop (ignore, just for testing)
Standard link just with added role
Test 1 expected: link is not activated, page is scrolled.
Test 2 expected: link is not activated, page scrolls and focus moves to next element.
Tab stop (ignore, just for testing)
Single event
Test 1 expected: page is not scrolled, link is activated.
Test 2 expected: link is activated (multiple times) and focus moves to next element.
Tab stop (ignore, just for testing)
Double event
Test 1 expected: page is not scrolled, link is activated.
Test 2 expected: link is not activated, focus moves to next element.