For the past 15 years (or so), I’ve used the autofocus attribute to move cursor focus to a given form input control upon page load. But, as I’ve become more attune to accessibility (a11y) concerns, I’ve become hesitant about stealing focus and potentially disrupting the feedback provided by assistive technologies. That said, I still really want that ease-of-access for myself. As such, I’ve added a custom Alpine.js directive to Big Sexy Poems that will move focus to the first form input when I press the (f) key.
Using keyboard shortcuts to move focus is a common pattern. Sites, such as GitHub, that provide a global search input, will use the (/) key to move focus to the…
For the past 15 years (or so), I’ve used the autofocus attribute to move cursor focus to a given form input control upon page load. But, as I’ve become more attune to accessibility (a11y) concerns, I’ve become hesitant about stealing focus and potentially disrupting the feedback provided by assistive technologies. That said, I still really want that ease-of-access for myself. As such, I’ve added a custom Alpine.js directive to Big Sexy Poems that will move focus to the first form input when I press the (f) key.
Using keyboard shortcuts to move focus is a common pattern. Sites, such as GitHub, that provide a global search input, will use the (/) key to move focus to the search form. I’ve chosen to use f for several reasons:
“Focus” starts with f.
“Form” starts with f.
The f key is on the left side of the keyboard, which means that for most people it’s the non-mouse hand. When navigating around the web, the non-mouse hand is generally at rest (waiting at the side of the keyboard), making it ready to active a key command.
When intercepting a keyboard event and using it to move focus, I have to make sure that I’m not stealing focus from another active element. Or, at least, an active element that is using that focus in a meaningful way.
I’m sure that I could go down a very deep rabbit hole when trying to be good about focus management. But, since I’m the one writing this ColdFusion application (Big Sexy Poems), I understand that it’s a finite set of constraints. When intercepting keydown events, I’m only checking:
Has the event been modified in any way?
Is the document’s activeElement some type of input control?
I’m not worrying about having multiple forms on one page; and I’m not worrying about modal / dialog windows since I don’t have any yet. My user interface is simple and I’m keeping my logic simple.
Unlike the autofocus attribute, which is applied to a form control, I’m applying my Alpine.js directive to the form itself. I don’t feel strongly about this decision; and, in fact, the more I think about it, the more I think I should have applied it to an input control in order to remove any confusion.
Aside: as I’m writing this post, I’ve convinced myself to go back and move the directive from the form element to the input element. It just makes more sense: less confusion, less dynamic, more explicit code. But, for the purposes of this post, I’m going to continue as-is.
The Alpine.js directive is applied to the form element using the x-keyed-focus attribute:
<form method="post" x-keyed-focus>
<input type="text" />
<input type="text" />
<button type="submit" />
</form>
When Alpine.js processes this DOM (Document Object Model) tree branch, it will instantiate the custom directive and apply it to the form element. I then bind my global keydown listener:
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.directive( "keyed-focus", KeyedFocusDirective );
}
);
/**
* I listen to the `F` key as a means to move focus to the first input within the form.
* This is being done to allow convenient focus without the accessibility concerns that
* come with the `autofocus` directive.
*/
function KeyedFocusDirective( element, metadata, framework ) {
var triggerKey = "f";
// CSS selector used to find eligible fields to focus.
var selector = `
input:not(:disabled, [hidden], [type='hidden'], [type='button'], [type='submit'], [type='image']),
textarea:not(:disabled, [hidden]),
select:not(:disabled, [hidden])
`;
init();
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I setup the directive.
*/
function init() {
framework.cleanup( destroy );
// If the directive has an expression, use this as the CSS selector when finding
// eligible elements to focus.
if ( metadata.expression ) {
selector = metadata.expression;
}
window.addEventListener( "keydown", handleKeydown );
}
/**
* I tear down the directive.
*/
function destroy() {
window.removeEventListener( "keydown", handleKeydown );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I handle the keydown event on the window.
*/
function handleKeydown( event ) {
// If the event was triggered for any other key, ignore event.
if ( event.key !== triggerKey ) {
return;
}
// If the key is modified in any way, ignore event. The user is doing something
// specific and we don't want to interfere.
if ( event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ) {
return;
}
// If the currently active element is an input variation, ignore event. The user
// may be working in a sibling form container and we don't want to steal focus.
if ( document.activeElement?.matches( "button, input, select, textarea" ) ) {
return;
}
var newTarget = element.querySelector( selector );
if ( newTarget ) {
// We're about to move focus to an input before the key life-cycle has
// completed. If we don't prevent the default behavior, the browser will
// render a character into the focused form control.
event.preventDefault();
newTarget.focus();
}
}
}
Aside: yeah... if I apply this directive to an input instead of to the form, that whole
selectorcomplexity will go away. Clearly, I should have gone with an input binding. Well, this is exactly why writing about this stuff is so helpful for me.
If I now open one of the form interfaces in Big Sexy Poems and hit the f key, we can see the focus move to the first input within the form:
I feel like this gives me all the convenience of the native autofocus attribute without disrupting any assistive technologies. But, the big issue now is that I have no idea how to make this discoverable. I know it’s there because I wrote it; but how will any other users know about it? I’ll have to create some sort of “Keyboard shortcuts” dialog or something.
But that sounds like a problem for “tomorrow Ben”.
Is autofocus Bad For Accessibility?
The truth is, I’m not sure. I thought this was a topic that would be really clear-cut when I started to looking into it. But, most sources just say, “maybe”. Even the Mozilla Developer Network (my Bible) is like “yeah”, but then also says “use caution”. So, what does “use caution” mean? I assume that means it’s OK sometimes, but when? Under what conditions?
Want to use code from this post? Check out the license.
I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
— Ben Nadel Managed hosting services provided by: