Dynamic links and focus

An exercise in usability

At Khan Academy, we’re currently redesigning the “Confirm your email address” banner.

In this article I’d like to zoom in a bit and highlight some of the accessibility gotchas that popped up during development — specifically with this seemingly trivial “Resend email” link.

Looks easy enough, right?

Markup: First Pass

Here’s the piece of markup that controls the link. It’s written using React (don’t mind the funky HTML-within-JavaScript), and returns one of two React elements based on the isSent state. Either an anchor tag which can be clicked to trigger the resend, or a plain old span for the confirmation “Sent!” message.

if (this.state.isSent) {
return <span>
Sent!
</span>;
} else {
return <a
href="javascript:void 0"
onClick={(e) => this.handleResendClick(e)}
>
Resend email
</a>;
}

Add in a dash of CSS (maybe also written in JavaScript?) and bam, we’re done with this feature.

But hold on, there are two things there that might make this feature difficult to use. Let’s look at them under a microscope.

Smell #1: Is that really a link?

<a href=”javascript:void 0"> is our first red flag. An anchor tag should really link to another piece of content, and preventing that navigation with “javascript:void 0” (or href=”#”) is counter-intuitive if we take a step back and look at what we’re building. Really we just want something we can click on, and we have the perfect element for that — the <button>.

So let’s go ahead and change our <a> to a <button>. We can remove the weird href, and now we’ll have ourselves a nice semantic element that screen-reader users understand as a “button” and not as a “link” that may take them somewhere else. We also get click events with the spacebar for free.

if (this.state.isSent) {
return <span>
Sent!
</span>;
} else {
return <button onClick={(e) => this.handleResendClick(e)}>
Resend email
</button>;
}

Smell #2: Focus management

Something equally subtle, but exceptionally more important, is how we manage our focus with this link. Currently we’re doing a bad job.

Consider the following gif of me tabbing around the site:

After clicking our “Resend email” button, you can see how the focus cursor disappears and I’m forced to start tabbing from the beginning of the page.

Our visitors using a mouse won’t be affected by this, but anyone using a keyboard for any number of reasons will be left in the dark, as their focus cursor is ripped out from right under them. Ideally, focus should remain on the resulting “Sent!” text, so that subsequent tabs or shift+tabs bring us forward and backward from the spot we expect to be.

In this case the banner is fairly close to the beginning of the document, but it’s still a few key presses we shouldn’t have to make.

So why’s this happening? If you recall our markup from before, we are transitioning from a <button> to a <span> once we click on “Resend email.” The <button> is swapped out, and the focus goes with it.

if (this.state.isSent) {
return <span>
Sent!
</span>;
} else {
return <button onClick={(e) => this.handleResendClick(e)}>
Resend email
</button>;
}

We can fix this in a few different ways. One common technique for dealing with dynamic content is to manually focus the element that’s been swapped in. It requires a bit of code to do this, though. First we’ll modify our render method to (a) allow the <span> to be focusable by use of the “tabindex” attribute and (b) store a reference to the span so we can focus it later.

render() {
if (this.state.isSent) {
return <span
tabindex="0"
ref={(node) => this.spanNode = node}
>
Sent!
</span>;
} else {
return <button onClick={(e) => this.handleResendClick(e)}>
Resend email
</button>;
}
}

Then we’ll add a callback to our setState in handleResendClick.

handleResendClick(e) {
this.setState({
isSent: true,
}, () => {
// After setting state, manually
// focus the new span
this.spanNode.focus();
});
}

Wonderful — now the focus cursor stays put and we’re free to tab forward and backward without needing to start at the beginning. We can do one step better though.

What if we didn’t remove the “Resend email” button at all? What if, instead, we kept the button and merely changed its appearance? What would “Sent!” look like as a button?

if (this.state.isSent) {
// Disable our button since clicking it no longer
// has an effect, but give it a tabindex of -1 so
// that it stays focused.
return <button disabled tabindex="-1">
Sent!
</button>;
} else {
return <button onClick={(e) => this.handleResendClick(e)}>
Resend email
</button>;
}

Once again the focus cursor stays put! Our original button is no longer removed from the DOM, and we can maintain focus without needing all that extra code to manually focus the new “Sent!” text.

Takeaways

Next time you come across an element that must change content when clicked, be sure to always keep in mind these two principles:

  • The user’s focus cursor should never be taken from them
  • Make sure you’re using the appropriate tag for the job

Doing so may require a bit of thought, but your interface will be usable in all sorts of unexpected scenarios, and your users will be forever grateful.

Thanks for reading :) Be sure to follow me on twitter where I rant a lot about this stuff and occasionally talk about my new tool, Shade — the greatest contrast tool in the galaxy 🚀

Extra reading